Podemos substituir completamente a herança usando padrão de estratégia e injeção de dependência?

10

Por exemplo:

var duckBehaviors = new Duckbehavior();
duckBehaviors.quackBehavior = new Quack();
duckBehaviors.flyBehavior = new FlyWithWings();
Duck mallardDuck = new Duck(DuckTypes.MallardDuck, duckBehaviors)

Como a classe Duck contém todos os comportamentos (abstrato), a criação de uma nova classe MallardDuck(que se estende Duck) não parece ser necessária.

Referência: Head First Design Pattern, Capítulo 1.

Deepak Mishra
fonte
Quais são os tipos de Duckbehavior.quackBehaviore outros campos 'no seu código?
precisa saber é
Não há injeção de dependência no seu exemplo.
David Conrad
11
A herança é incrível quando você é desenvolvedor de nível intermediário a médio e alto, porque há uma longa história de truques de refatoração e padrões de design em torno dele. Mas os desenvolvedores mais experientes com quem conversei preferem hierarquias de herança realmente rasas com base na composição sempre que possível. A herança pode causar um acoplamento mais rígido do que o necessário e pode dificultar as alterações.
Mark Rogers
5
@ DavidConrad: O OP não está fazendo as novidades dentro da classe . O DI / IoC trata de injetar dependências, não de contêineres ou de nunca usar "novo"; essa é uma preocupação inteiramente ortogonal. Além disso, você sempre precisa alterar o código em algum lugar - seja a raiz da composição ou algum arquivo de configuração. O controle é invertido aqui no tipo Duck, pois o que controla a criação das dependências não é o Duck ctor, mas algum contexto externo; sendo este um exemplo de brinquedo dado para maior clareza, é perfeitamente aceitável que o contexto externo seja representado apenas pelo código de chamada.
Filip Milovanović

Respostas:

21

Claro, mas chamamos isso de composição e delegação . O padrão de estratégia e a injeção de dependência podem parecer estruturalmente semelhantes, mas suas intenções são diferentes.

O Padrão de Estratégia permite a modificação do comportamento em tempo de execução na mesma interface. Eu poderia dizer a um pato-real para voar e vê-lo voar com asas. Troque-o por um pato piloto de jato e observe-o voar com as companhias aéreas da Delta. Fazer isso enquanto o programa está em execução é uma coisa do Padrão de Estratégia.

A Injeção de Dependência é uma técnica para evitar dependências de código rígido, para que elas possam mudar independentemente, sem exigir que os clientes sejam modificados quando mudarem. Os clientes simplesmente expressam suas necessidades sem saber como serão atendidas. Assim, como eles são cumpridos é decidido em outro lugar (geralmente no principal). Você não precisa de dois patos para fazer uso dessa técnica. Apenas algo que usa um pato sem saber ou se importar com qual pato. Algo que não constrói o pato ou procura, mas fica perfeitamente feliz em usar o pato que você lhe der.

Se eu tenho uma classe de pato concreta, ela pode implementar o comportamento da mosca. Eu poderia até mudar comportamentos de voar com asas para voar com Delta com base em uma variável de estado. Essa variável pode ser um booleano, um int ou FlyBehaviorum flymétodo que faça qualquer estilo de execução sem que eu precise testá-la com um if. Agora eu posso mudar os estilos de vôo sem alterar os tipos de patos. Agora os Marreco podem se tornar pilotos. Isso é composição e delegação . O pato é composto de um FlyBehavior e pode delegar solicitações de vôo a ele. Você pode substituir todos os seus comportamentos de pato de uma só vez dessa maneira ou manter algo para cada comportamento ou qualquer combinação entre eles.

Isso lhe dá todos os mesmos poderes que a herança possui, exceto um. A herança permite expressar quais métodos do Duck você está substituindo nos subtipos de Duck. A composição e a delegação exigem que o Duck delegue explicitamente os subtipos desde o início. Isso é muito mais flexível, mas envolve mais digitação do teclado e o Duck precisa saber que está acontecendo.

No entanto, muitas pessoas acreditam que a herança deve ser explicitamente projetada desde o início. E que, se não tiver sido, você deve marcar suas aulas como seladas / finais para proibir a herança. Se você adota essa visão, a herança realmente não tem vantagem sobre a composição e a delegação. Porque, de qualquer maneira, é necessário projetar a extensibilidade desde o início ou estar disposto a derrubar as coisas mais tarde.

Derrubar as coisas é realmente uma opção popular. Esteja ciente de que há casos em que é um problema. Se você implantou bibliotecas ou módulos de código de forma independente que não pretende atualizar com a próxima versão, pode acabar lidando com versões de classes que não sabem nada sobre o que você está fazendo agora.

Embora esteja disposto a derrubar as coisas mais tarde, você pode liberar o excesso de projeto; há algo muito poderoso em ser capaz de projetar algo que use um pato sem precisar saber o que o pato realmente fará quando usado. Que não saber é algo poderoso. Ele permite que você pare de pensar em patos por um tempo e pense no restante do seu código.

"Podemos" e "devemos" são perguntas diferentes. Favorecer a composição sobre a herança não diz nunca usar herança. Ainda existem casos em que a herança faz mais sentido. Vou mostrar meu exemplo favorito :

public class LoginFailure : System.ApplicationException {}

A herança permite criar exceções com nomes descritivos e mais específicos em apenas uma linha.

Tente fazer isso com a composição e você terá uma bagunça. Além disso, não há risco do problema ioiô de herança porque não há dados ou métodos aqui para reutilizar e incentivar o encadeamento de herança. Tudo isso acrescenta um bom nome. Nunca subestime o valor de um bom nome.

candied_orange
fonte
1
" Nunca subestime o valor de um bom nome ". Novo tempo de roupas do imperador: LoginExceptionnão é um bom nome. É o clássico "nome do smurf" e, se eu tirar Exception, tudo o que tenho é Loginque não me diz nada do que deu errado.
David Arno
Infelizmente, estamos presos à ridícula convenção de Exceptionencerrar todas as classes de exceção, mas não confunda "preso a ele" com "bom".
David Arno
@DavidArno Se você pode sugerir um nome melhor, sou todo ouvidos. Aqui temos o benefício de não ficar preso nas convenções de uma base de código existente. Se você tivesse o poder de mudar o mundo com um estalar de dedos, como o chamaria?
candied_orange
Eu nomeá-los, StackOverflow, OutOfMemory, NullReferenceAccess, LoginFailureetc. Basicamente, tomar "Exception" fora do nome. E, se necessário, corrija-os para que descreva o que deu errado.
David Arno
@DavidArno por seu comando.
candied_orange
7

Você pode substituir quase qualquer metodologia por qualquer outra metodologia e ainda produzir software funcional. No entanto, alguns são mais adequados para um problema específico do que outros.

Depende de muitas coisas que é preferível. Técnica anterior no aplicativo, experiência na equipe, desenvolvimentos futuros esperados, preferência pessoal e quão difícil será para um iniciante entender o assunto, para citar alguns.

À medida que você se torna mais experiente e se debate com mais frequência com as criações de outras pessoas, provavelmente dará mais ênfase à última contemplação.

A herança ainda é uma ferramenta de modelagem válida e forte, que nem sempre é a mais flexível, mas oferece uma orientação forte a novas pessoas que podem ser gratas pelo mapeamento claro do domínio do problema.

Martin Maat
fonte
-1

A herança não é tão importante quanto se pensava. Ainda é importante , e removê-lo seria um erro grave.

Um exemplo extremo é o Objective-C, onde tudo é uma subclasse de NSObject (o compilador, na verdade, não permite que você declare uma classe que não possui uma classe básica, portanto, tudo o que não está incorporado no compilador deve ser uma subclasse de algo embutido no compilador). Há muitas coisas úteis incorporadas ao NSObject que você não precisaria perder.

Outro exemplo interessante é o UIView, a classe "view" básica no desenvolvimento do UIKit para iOS. Esta é uma classe que é, por um lado, um pouco como uma classe abstrata que declara a funcionalidade que as subclasses devem preencher, mas também é útil por si só. Possui subclasses fornecidas pelo UIKit, que são frequentemente usadas como estão. Há composição pelo desenvolvedor que instala subvisões em uma visualização. E muitas vezes existem subclasses definidas pelo desenvolvedor, que costumam usar composição. Não existe uma regra estrita ou única, você usa o que melhor se adapta aos seus requisitos.

gnasher729
fonte
O seu "exemplo extremo" é mais ou menos como mais modernas linguagens OO função: subclasses do Python type, todos os não-primitiva em herda Java a partir Object, tudo em JavaScript tem um protótipo terminando com Object, etc.
Delioth
-1

[Arriscarei uma resposta atrevida à pergunta titular.]

Podemos substituir completamente a herança usando padrão de estratégia e injeção de dependência?

Sim ... exceto que o próprio padrão de estratégia usa herança. O padrão de estratégia funciona na herança da interface. Substituir herança por composição + estratégia move a herança para um local diferente. No entanto, muitas vezes vale a pena fazer essa substituição, pois permite separar hierarquias .

Nick Alexeev
fonte
2
" O padrão de estratégia funciona na herança da interface ". Lembre-se, o padrão de estratégia é um padrão de design; não é um padrão de implementação. Portanto, ele pode ser implementado usando interfaces, mas também pode ser implementado usando, por exemplo, um hash / dicionário de funções.
David Arno
3
A herança de interface não é herança, é apenas um tipo de contrato ou classificador. Você pode usar delegados também para o padrão de estratégia (eu prefiro usá-los).
Deepak Mishra
Mais um, cara: ... trabalha com herança de interface , parabéns pelo uso adequado do termo geral "interface". Eu acho que o design de classe ruim ou incompetente é a razão básica pela qual essa questão é levantada. Explicar por que / como levaria um livro; O diabo está nos detalhes. Eu trabalhei com projetos de herança que eram uma alegria de se ver e, ao mesmo tempo, uma herança profunda que era um inferno. Eu já vi interfacedesign craptástico consertado com herança. O problema oculto aqui é a implementação inconsistente de transformar comportamentos dependentes. P, S. herança e interface não são mutuamente exclusivas,
radarbob
Comentário do Davididno : Esse comentário seria bom se anexado à pergunta do OP. A questão do OP é tecnicamente incorreta, mas ainda a entendemos. A questão não é essa resposta específica.
Radarbob 19/06/19
@DeepakMishra comentário: . Uma "interface" é todos os membros públicos de uma classe. Infelizmente, o significado da "interface" está sobrecarregado. Devemos ter cuidado para distinguir o significado geral com chave de uma linguagem de programaçãointerface
radarbob
-2

Não. Se um pato-real exige parâmetros diferentes para instanciar do que qualquer outro tipo de pato, seria um pesadelo mudar. Você também deve ser capaz de instanciar um pato? É mais um pato-real ou pato mandarim ou qualquer outro tipo de patinho que você tenha.

Também posso querer alguma lógica em torno dos tipos, para que uma hierarquia de tipos possa ser melhor.

Agora, se a reutilização de código se tornar problemática para algumas das funcionalidades, poderíamos compor passando as funções pelo construtor.

Novamente, isso realmente depende do seu caso de uso. Se você tem apenas um pato e um pato-real, uma hierarquia de classes é uma solução muito mais simples.

Mas aqui está um exemplo em que você deseja usar o padrão de estratégia: se você tem classes de clientes e deseja passar uma estratégia de cobrança (interface) que pode ser uma estratégia de cobrança de happy hour ou estratégia de cobrança regular, você pode passá-lo no construtor. Dessa forma, você não precisa criar duas classes diferentes de clientes e não precisa de uma hierarquia. Você apenas tem a classe de um cliente.

Benjamin Erichsen
fonte