Por que preferir composição a herança? Que trade-offs existem para cada abordagem? Quando você deve escolher herança e não composição?
Por que preferir composição a herança? Que trade-offs existem para cada abordagem? Quando você deve escolher herança e não composição?
Respostas:
Prefira composição sobre herança, pois é mais maleável / fácil de modificar posteriormente, mas não use uma abordagem de composição sempre. Com a composição, é fácil alterar o comportamento em tempo real com os Injetores / Setters de Dependência. A herança é mais rígida, pois a maioria dos idiomas não permite derivar de mais de um tipo. Assim, o ganso é mais ou menos cozido quando você deriva do Tipo A.
Meu teste de ácido para o acima é:
O TypeB deseja expor a interface completa (todos os métodos públicos) do TypeA, de modo que o TypeB possa ser usado onde o TypeA é esperado? Indica herança .
O TypeB deseja apenas parte do comportamento exposto pelo TypeA? Indica necessidade de composição.
Atualização: Acabei de voltar à minha resposta e parece que agora ela está incompleta sem uma menção específica ao Princípio de Substituição de Liskov de Barbara Liskov como um teste para 'Devo herdar deste tipo?'
Pense na contenção como um relacionamento. Um carro "tem um" motor, uma pessoa "tem um" nome etc.
Pense na herança como um relacionamento. Um carro "é um" veículo, uma pessoa "é um" mamífero, etc.
Não aceito crédito por essa abordagem. Eu peguei direto da Segunda Edição do Code Complete de Steve McConnell , Seção 6.3 .
Se você entende a diferença, é mais fácil explicar.
Um exemplo disso é o PHP sem o uso de classes (principalmente antes do PHP5). Toda lógica é codificada em um conjunto de funções. Você pode incluir outros arquivos que contenham funções auxiliares e assim por diante e conduzir sua lógica de negócios passando dados em funções. Isso pode ser muito difícil de gerenciar à medida que o aplicativo cresce. O PHP5 tenta remediar isso oferecendo um design mais orientado a objetos.
Isso incentiva o uso de classes. A herança é um dos três princípios do projeto OO (herança, polimorfismo, encapsulamento).
class Person {
String Title;
String Name;
Int Age
}
class Employee : Person {
Int Salary;
String Title;
}
Isso é herança no trabalho. O Empregado "é uma" Pessoa ou herda de Pessoa. Todos os relacionamentos de herança são relacionamentos "is-a". O Employee também oculta a propriedade Title de Person, ou seja, Employee.Title retornará o Title para o Employee, e não a Person.
A composição é favorecida sobre a herança. Para simplificar, você teria:
class Person {
String Title;
String Name;
Int Age;
public Person(String title, String name, String age) {
this.Title = title;
this.Name = name;
this.Age = age;
}
}
class Employee {
Int Salary;
private Person person;
public Employee(Person p, Int salary) {
this.person = p;
this.Salary = salary;
}
}
Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);
A composição é tipicamente "tem um" ou "usa um" relacionamento. Aqui a classe Employee tem uma Person. Ele não herda de Person, mas recebe o objeto Person, e é por isso que ele "possui uma" Person.
Agora diga que você deseja criar um tipo de gerente para terminar com:
class Manager : Person, Employee {
...
}
Este exemplo funcionará bem, no entanto, e se Person e Employee ambos declarassem Title
? O Manager.Title deve retornar "Manager of Operations" ou "Mr."? Sob composição, essa ambiguidade é melhor tratada:
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
O objeto Manager é composto como um funcionário e uma pessoa. O comportamento do título é obtido do funcionário. Essa composição explícita remove a ambiguidade entre outras coisas e você encontrará menos erros.
Com todos os benefícios inegáveis fornecidos pela herança, eis algumas de suas desvantagens.
Desvantagens da herança:
Por outro lado, a composição de objetos é definida em tempo de execução através de objetos que adquirem referências a outros objetos. Nesse caso, esses objetos nunca poderão alcançar os dados protegidos um do outro (sem quebra de encapsulamento) e serão forçados a respeitar a interface um do outro. E também neste caso, as dependências de implementação serão muito menores do que no caso de herança.
Outro motivo, muito pragmático, para preferir composição a herança, tem a ver com o modelo de domínio e o mapeamento para um banco de dados relacional. É realmente difícil mapear a herança para o modelo SQL (você acaba com todos os tipos de soluções alternativas, como criar colunas que nem sempre são usadas, usar visualizações etc.). Algumas ORMLs tentam lidar com isso, mas sempre fica complicado rapidamente. A composição pode ser facilmente modelada por meio de um relacionamento de chave estrangeira entre duas tabelas, mas a herança é muito mais difícil.
Embora, em poucas palavras, eu concorde com "Prefira composição sobre herança", muitas vezes para mim parece "prefira batata em vez de coca-cola". Há lugares para herança e lugares para composição. Você precisa entender a diferença, então esta questão desaparecerá. O que realmente significa para mim é "se você vai usar herança - pense novamente, é provável que precise de composição".
Você deve preferir batatas ao invés de coca-cola quando quiser comer e coca-cola ao invés de batatas quando quiser beber.
Criar uma subclasse deve significar mais do que apenas uma maneira conveniente de chamar métodos de superclasse. Você deve usar herança quando a superclasse "subclasse" é uma "tanto estrutural quanto funcionalmente, quando pode ser usada como superclasse e você a usará. Se não for o caso - não é herança, mas outra coisa. Composição é quando seus objetos consistem em outro ou têm algum relacionamento com eles.
Então, para mim, parece que se alguém não sabe se precisa de herança ou composição, o verdadeiro problema é que ele não sabe se quer beber ou comer. Pense mais no domínio do seu problema, entenda-o melhor.
InternalCombustionEngine
com uma classe derivada GasolineEngine
. O último adiciona itens como velas de ignição, que a classe base não possui, mas o uso como um InternalCombustionEngine
fará com que as velas sejam usadas.
A herança é bastante atraente, principalmente proveniente de terras processuais e, muitas vezes, parece enganosamente elegante. Quero dizer, tudo o que preciso fazer é adicionar esse pouco de funcionalidade a alguma outra classe, certo? Bem, um dos problemas é que
Sua classe base quebra o encapsulamento expondo os detalhes da implementação às subclasses na forma de membros protegidos. Isso torna seu sistema rígido e frágil. A falha mais trágica, no entanto, é a nova subclasse traz toda a bagagem e opinião da cadeia de herança.
O artigo, Herança é má: a falha épica do DataAnnotationsModelBinder , mostra um exemplo disso em C #. Ele mostra o uso da herança quando a composição deveria ter sido usada e como ela poderia ser refatorada.
Em Java ou C #, um objeto não pode alterar seu tipo depois de ter sido instanciado.
Portanto, se seu objeto precisar aparecer como um objeto diferente ou se comportar de maneira diferente, dependendo do estado ou das condições do objeto, use Composition : Consulte Padrões de Design de Estado e Estratégia .
Se o objeto precisar ser do mesmo tipo, use Herança ou implemente interfaces.
Client
. Então, um novo conceito de um PreferredClient
aparece mais tarde. Deve PreferredClient
herdar Client
? Um cliente preferido 'é um' cliente afinal, não? Bem, não tão rápido ... como você disse, os objetos não podem mudar de classe em tempo de execução. Como você modelaria a client.makePreferred()
operação? Talvez a resposta esteja no uso da composição com um conceito ausente, Account
talvez?
Client
aulas, talvez haja apenas um que encapsula o conceito de Account
que poderia ser um StandardAccount
ou uma PreferredAccount
...
Não encontrei uma resposta satisfatória aqui, então escrevi uma nova.
Para entender por que " preferir composição a herança", precisamos primeiro recuperar a suposição omitida nesse idioma abreviado.
Existem dois benefícios da herança: subtipagem e subclassificação
Subtipagem significa estar em conformidade com uma assinatura de tipo (interface), ou seja, um conjunto de APIs, e pode-se substituir parte da assinatura para obter polimorfismo de subtipagem.
Subclassificação significa reutilização implícita de implementações de métodos.
Com os dois benefícios, surgem dois propósitos diferentes para a herança: orientação para subtipagem e orientação para reutilização de código.
Se a reutilização de código for o único objetivo, a subclasse pode dar mais do que o necessário, ou seja, alguns métodos públicos da classe pai não fazem muito sentido para a classe filho. Nesse caso, em vez de favorecer a composição sobre a herança, a composição é exigida . É também aqui que a noção "é-a" vs. "tem-a" vem.
Portanto, somente quando a subtipagem é proposta, ou seja, para usar a nova classe posteriormente de maneira polimórfica, enfrentamos o problema de escolher herança ou composição. Essa é a suposição que é omitida no idioma reduzido em discussão.
Subtipo é estar em conformidade com uma assinatura de tipo, isso significa que a composição sempre deve expor não menos quantidade de APIs do tipo. Agora, as compensações começam:
A herança fornece reutilização direta de código, se não for substituída, enquanto a composição precisa re-codificar cada API, mesmo que seja apenas um simples trabalho de delegação.
A herança fornece recursão aberta direta através do site polimórfico interno this
, ou seja, invocando o método de substituição (ou mesmo o tipo ) em outra função membro, pública ou privada (embora desencorajada ). A recursão aberta pode ser simulada via composição , mas requer um esforço extra e nem sempre é viável (?). Esta resposta a uma pergunta duplicada fala algo semelhante.
Herança expõe protegido membros . Isso quebra o encapsulamento da classe pai e, se usado pela subclasse, outra dependência entre o filho e seu pai é introduzida.
A composição é adequada à inversão de controle e sua dependência pode ser injetada dinamicamente, como é mostrado no padrão decorador e no padrão proxy .
A composição tem o benefício da programação orientada a combinadores , ou seja, funciona de maneira semelhante ao padrão composto .
A composição segue imediatamente a programação para uma interface .
A composição tem o benefício da herança múltipla fácil .
Com as compensações acima em mente, preferimos , portanto, composição do que herança. No entanto, para classes estreitamente relacionadas, ou seja, quando a reutilização implícita de código realmente traz benefícios, ou o poder mágico da recursão aberta é desejado, a herança deve ser a escolha.
Pessoalmente, aprendi a sempre preferir composição a herança. Não há problema programático que você possa resolver com herança que não possa ser resolvido com composição; embora você precise usar interfaces (Java) ou protocolos (Obj-C) em alguns casos. Como o C ++ não sabe disso, você precisará usar classes base abstratas, o que significa que não pode se livrar totalmente da herança no C ++.
A composição geralmente é mais lógica, fornece melhor abstração, melhor encapsulamento, melhor reutilização de código (especialmente em projetos muito grandes) e tem menos probabilidade de quebrar qualquer coisa à distância, apenas porque você fez uma alteração isolada em qualquer lugar do código. Também facilita a manutenção do " Princípio da Responsabilidade Única ", que muitas vezes é resumido como " Nunca deve haver mais de um motivo para uma classe mudar. ", E isso significa que toda classe existe para um propósito específico e deve só tem métodos diretamente relacionados ao seu objetivo. Também ter uma árvore de herança muito rasa facilita muito a manutenção da visão geral, mesmo quando o projeto começa a ficar realmente grande. Muitas pessoas pensam que a herança representa o nosso mundo real Muito bem, mas essa não é a verdade. O mundo real usa muito mais composição do que herança .. Praticamente todos os objetos do mundo real que você pode segurar na mão foram compostos de outros objetos menores do mundo real.
Existem desvantagens da composição, no entanto. Se você pular a herança completamente e se concentrar apenas na composição, notará que muitas vezes é necessário escrever algumas linhas de código extras que não seriam necessárias se você tivesse usado a herança. Às vezes, você também é forçado a se repetir e isso viola o Princípio DRY(SECO = Não se repita). Além disso, a composição geralmente requer delegação, e um método está apenas chamando outro método de outro objeto sem outro código em torno dessa chamada. Essas "chamadas de método duplo" (que podem facilmente se estender a chamadas de método triplas ou quádruplas e até mais além disso) têm desempenho muito pior que a herança, onde você simplesmente herda o método de seus pais. Chamar um método herdado pode ser igualmente rápido como chamar um não herdado, ou pode ser um pouco mais lento, mas geralmente ainda é mais rápido do que duas chamadas de método consecutivas.
Você deve ter notado que a maioria dos idiomas OO não permite herança múltipla. Embora existam alguns casos em que a herança múltipla pode realmente comprar alguma coisa, mas essas são exceções e não a regra. Sempre que você se deparar com uma situação em que você acha que "herança múltipla seria um recurso muito interessante para resolver esse problema", geralmente você está em um ponto em que deve repensar completamente a herança, pois mesmo isso pode exigir algumas linhas de código extras , uma solução baseada na composição geralmente se mostrará muito mais elegante, flexível e à prova de futuro.
A herança é realmente um recurso interessante, mas receio que tenha sido usado em excesso nos últimos dois anos. As pessoas tratavam a herança como o único martelo capaz de pregar tudo, independentemente de se tratar de um prego, um parafuso ou talvez algo completamente diferente.
TextFile
é um File
.
Minha regra geral: antes de usar a herança, considere se a composição faz mais sentido.
Razão: Subclassificação geralmente significa mais complexidade e conexão, ou seja, mais difíceis de mudar, manter e dimensionar sem cometer erros.
Uma resposta muito mais completa e concreta de Tim Boudreau, da Sun:
Problemas comuns ao uso da herança como eu vejo são:
- Atos inocentes podem ter resultados inesperados - O exemplo clássico disso é chamar métodos substituíveis do construtor da superclasse, antes que os campos da instância das subclasses tenham sido inicializados. Em um mundo perfeito, ninguém faria isso. Este não é um mundo perfeito.
- Oferece tentações perversas para os subclassers fazerem suposições sobre a ordem das chamadas de método e tais - tais suposições tendem a não ser estáveis se a superclasse evoluir ao longo do tempo. Veja também a analogia da minha torradeira e cafeteira .
- As aulas ficam mais pesadas - você não sabe necessariamente o trabalho que sua superclasse está fazendo em seu construtor ou quanta memória será usada. Portanto, construir algum objeto leve e inocente pode ser muito mais caro do que você pensa, e isso pode mudar com o tempo se a superclasse evoluir
- Encoraja uma explosão de subclasses . O carregamento de classes custa tempo, mais classes custa memória. Isso pode não ser um problema até que você esteja lidando com um aplicativo na escala do NetBeans, mas tivemos problemas reais, por exemplo, com menus sendo lentos porque a primeira exibição de um menu acionou o carregamento maciço de classe. Corrigimos isso passando para uma sintaxe mais declarativa e outras técnicas, mas que também custam tempo para serem corrigidas.
- Torna mais difícil mudar as coisas mais tarde - se você tornou uma classe pública, a troca da superclasse vai quebrar subclasses - é uma escolha com a qual, depois de tornar o código público, você é casado. Portanto, se você não estiver alterando a funcionalidade real da sua superclasse, terá muito mais liberdade para mudar as coisas mais tarde, se usar, em vez de estender o que precisa. Veja, por exemplo, a subclasse de JPanel - isso geralmente está errado; e se a subclasse for pública em algum lugar, você nunca terá a chance de revisar essa decisão. Se for acessado como JComponent getThePanel (), você ainda poderá fazê-lo (dica: expor modelos para os componentes contidos na API).
- As hierarquias de objetos não são dimensionadas (ou torná-las mais tarde é muito mais difícil do que planejar com antecedência) - esse é o problema clássico de "muitas camadas". Vou abordar isso abaixo e como o padrão AskTheOracle pode resolvê-lo (embora possa ofender os puristas da OOP).
...
Minha opinião sobre o que fazer, se você permitir a herança, que você pode usar com um grão de sal é:
- Nunca exponha campos, exceto constantes
- Os métodos devem ser abstratos ou finais
- Não chame nenhum método do construtor da superclasse
...
tudo isso se aplica menos a projetos pequenos do que grandes, e menos a aulas particulares do que públicas
Veja outras respostas.
Costuma-se dizer que uma classe Bar
pode herdar uma classe Foo
quando a seguinte frase for verdadeira:
- um bar é um foo
Infelizmente, o teste acima sozinho não é confiável. Use o seguinte:
- um bar é um foo, E
- as barras podem fazer tudo o que os foos podem fazer.
Os primeiros testes garante que todos os getters de Foo
fazer sentido no Bar
(= propriedades partilhadas), enquanto o segundo teste garante que todos os formadores de Foo
fazer sentido em Bar
(funcionalidade = partilhada).
Exemplo 1: Cão -> Animal
Um cão é um animal E os cães podem fazer tudo o que os animais podem fazer (como respirar, morrer, etc.). Portanto, a classe Dog
pode herdar a classe Animal
.
Exemplo 2: Círculo - / -> Elipse
Um círculo é uma elipse, MAS os círculos não podem fazer tudo o que as elipses podem fazer. Por exemplo, os círculos não podem se esticar, enquanto as elipses podem. Portanto, a classe Circle
não pode herdar a classe Ellipse
.
Isso é chamado de problema Circle-Ellipse , o que realmente não é um problema, apenas uma prova clara de que o primeiro teste por si só não é suficiente para concluir que a herança é possível. Em particular, este exemplo destaca que as classes derivadas devem estender a funcionalidade das classes base, nunca a restringir . Caso contrário, a classe base não poderia ser usada polimorficamente.
Mesmo que você possa usar herança não significa que deveria : usar composição é sempre uma opção. A herança é uma ferramenta poderosa que permite a reutilização implícita de códigos e o envio dinâmico, mas apresenta algumas desvantagens, razão pela qual a composição é frequentemente preferida. As trocas entre herança e composição não são óbvias e, na minha opinião, são melhor explicadas na resposta da lcn .
Como regra geral, costumo escolher herança sobre composição, quando se espera que o uso polimórfico seja muito comum; nesse caso, o poder do despacho dinâmico pode levar a uma API muito mais legível e elegante. Por exemplo, ter uma classe polimórfica Widget
em estruturas de GUI ou uma classe polimórficaNode
nas bibliotecas XML permite ter uma API muito mais legível e intuitiva de usar do que o que você teria com uma solução puramente baseada em composição.
Só para você saber, outro método usado para determinar se a herança é possível é chamado de Princípio de Substituição de Liskov :
Funções que usam ponteiros ou referências a classes base devem poder usar objetos de classes derivadas sem saber
Essencialmente, isso significa que a herança é possível se a classe base puder ser usada polimorficamente, o que acredito ser equivalente ao nosso teste "uma barra é um foo e as barras podem fazer tudo o que os foos podem fazer".
computeArea(Circle* c) { return pi * square(c->radius()); }
. É obviamente quebrado se passado uma elipse (o que significa raio ()?). Uma elipse não é um círculo e, como tal, não deve derivar do círculo.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Agora é genérico.
width()
e height()
? E se agora um usuário da biblioteca decidir criar outra classe chamada "EggShape"? Deveria também derivar de "Circle"? Claro que não. Uma forma de ovo não é um círculo, e uma elipse também não é um círculo; portanto, ninguém deve derivar do Círculo, pois quebra o LSP. Os métodos que executam operações em uma classe Circle * fazem suposições fortes sobre o que é um círculo, e quebrar essas suposições quase certamente levará a erros.
A herança é muito poderosa, mas você não pode forçá-la (veja: o problema da elipse circular ). Se você realmente não pode ter certeza absoluta de uma verdadeira relação de subtipo "é-a", é melhor seguir a composição.
A herança cria um forte relacionamento entre uma subclasse e uma superclasse; A subclasse deve estar ciente dos detalhes de implementação da superclasse. Criar a superclasse é muito mais difícil, quando você precisa pensar em como ela pode ser estendida. Você deve documentar os invariantes de classe com cuidado e declarar quais outros métodos substituíveis métodos usam internamente.
Às vezes, a herança é útil, se a hierarquia realmente representa um relacionamento. Está relacionado ao Princípio Aberto-Fechado, que afirma que as classes devem ser fechadas para modificação, mas abertas à extensão. Dessa forma, você pode ter polimorfismo; para ter um método genérico que lide com o supertipo e seus métodos, mas, por despacho dinâmico, o método da subclasse é chamado. Isso é flexível e ajuda a criar indiretos, o que é essencial no software (para saber menos sobre os detalhes da implementação).
Porém, a herança é facilmente usada em excesso e cria complexidade adicional, com grandes dependências entre as classes. Também é difícil entender o que acontece durante a execução de um programa devido a camadas e seleção dinâmica de chamadas de método.
Eu sugeriria usar a composição como padrão. É mais modular e oferece o benefício da ligação tardia (você pode alterar o componente dinamicamente). Também é mais fácil testar as coisas separadamente. E se você precisar usar um método de uma classe, não será obrigado a ter uma determinada forma (Princípio de Substituição de Liskov).
Inheritance is sometimes useful... That way you can have polymorphism
como vinculativo dos conceitos de herança e polimorfismo (subtipo assumido de acordo com o contexto). Meu comentário pretendia apontar o que você esclarece em seu comentário: que a herança não é a única maneira de implementar o polimorfismo e, de fato, não é necessariamente o fator determinante na decisão entre composição e herança.
Suponha que uma aeronave tenha apenas duas partes: um motor e asas.
Depois, existem duas maneiras de projetar uma classe de aeronave.
Class Aircraft extends Engine{
var wings;
}
Agora sua aeronave pode começar com asas fixas
e alterá-las para asas rotativas em tempo real. É essencialmente
um motor com asas. Mas e se eu quisesse trocar
o motor rapidamente?
A classe base Engine
expõe um mutador para alterar suas
propriedades ou redesenho Aircraft
como:
Class Aircraft {
var wings;
var engine;
}
Agora, também posso substituir meu motor em tempo real.
Você precisa dar uma olhada no Princípio de substituição de Liskov nos princípios SOLID de design de classe do tio Bob . :)
Quando você deseja "copiar" / expor a API da classe base, você usa herança. Quando você quiser apenas "copiar" a funcionalidade, use a delegação.
Um exemplo disso: você deseja criar uma pilha fora de uma lista. A pilha possui apenas pop, push e peek. Você não deve usar herança, já que não deseja funcionalidade push_back, push_front, removeAt, et al.-type em uma pilha.
Essas duas maneiras podem viver juntas muito bem e realmente se apoiar.
A composição está apenas reproduzindo-o modular: você cria uma interface semelhante à classe pai, cria um novo objeto e delega chamadas para ele. Se esses objetos não precisam se conhecer, é bastante seguro e fácil usar a composição. Existem tantas possibilidades aqui.
No entanto, se por algum motivo a classe pai precisar acessar as funções fornecidas pela "classe filha" para programadores inexperientes, pode parecer que ele é um ótimo local para usar herança. A classe pai pode simplesmente chamar seu próprio resumo "foo ()", que é sobrescrito pela subclasse e, em seguida, pode atribuir o valor à base abstrata.
Parece uma boa idéia, mas, em muitos casos, é melhor dar à classe um objeto que implemente o foo () (ou até definir o valor fornecido manualmente pelo foo ()) do que herdar a nova classe de alguma classe base que exija a função foo () a ser especificada.
Por quê?
Porque a herança é uma maneira ruim de mover informações .
A composição tem uma vantagem real aqui: o relacionamento pode ser revertido: a "classe pai" ou o "trabalhador abstrato" podem agregar quaisquer objetos "filhos" específicos que implementam determinada interface + qualquer filho pode ser definido dentro de qualquer outro tipo de pai, que aceite é tipo . E pode haver qualquer número de objetos, por exemplo, MergeSort ou QuickSort podem classificar qualquer lista de objetos implementando uma interface Compare abstrata. Ou, dito de outra maneira: qualquer grupo de objetos que implementa "foo ()" e outro grupo de objetos que podem fazer uso de objetos com "foo ()" podem tocar juntos.
Eu posso pensar em três razões reais para usar herança:
Se isso é verdade, provavelmente é necessário usar a herança.
Não há nada ruim em usar o motivo 1, é muito bom ter uma interface sólida em seus objetos. Isso pode ser feito usando composição ou com herança, não há problema - se essa interface for simples e não mudar. Geralmente, a herança é bastante eficaz aqui.
Se o motivo for o número 2, fica um pouco complicado. Você realmente só precisa usar a mesma classe base? Em geral, apenas o uso da mesma classe base não é bom o suficiente, mas pode ser um requisito da sua estrutura, uma consideração de design que não pode ser evitada.
No entanto, se você quiser usar as variáveis privadas, o caso 3, poderá estar com problemas. Se você considerar variáveis globais inseguras, considere usar a herança para obter acesso a variáveis privadas também inseguras . Lembre-se, as variáveis globais não são tão ruins assim - os bancos de dados são essencialmente um grande conjunto de variáveis globais. Mas se você puder lidar com isso, tudo bem.
Para resolver essa questão de uma perspectiva diferente para os programadores mais recentes:
A herança geralmente é ensinada cedo quando aprendemos a programação orientada a objetos, por isso é vista como uma solução fácil para um problema comum.
Eu tenho três classes que todas precisam de alguma funcionalidade comum. Portanto, se eu escrever uma classe base e todas herdarem dela, todas elas terão essa funcionalidade e só precisarei mantê-la em um único local.
Parece ótimo, mas na prática quase nunca funciona por um dos vários motivos:
No final, amarramos nosso código em alguns nós difíceis e não obtemos nenhum benefício, exceto pelo fato de dizermos: "Legal, eu aprendi sobre herança e agora eu a usei". Isso não deve ser condescendente, porque todos nós fizemos isso. Mas todos nós fizemos isso porque ninguém nos disse para não fazer.
Assim que alguém me explicou "favorecer a composição sobre a herança", pensei em todas as vezes que tentava compartilhar a funcionalidade entre as classes usando herança e percebi que na maioria das vezes não funcionava bem.
O antídoto é o Princípio da Responsabilidade Única . Pense nisso como uma restrição. Minha turma deve fazer uma coisa. I deve ser capaz de dar a minha aula um nome que de alguma maneira descreve que uma coisa que ele faz. (Há exceções em tudo, mas as regras absolutas às vezes são melhores quando estamos aprendendo.) Daqui resulta que não posso escrever uma classe base chamada ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. Qualquer que seja a funcionalidade distinta que eu precise, deve estar em sua própria classe e, em seguida, outras classes que precisam dessa funcionalidade podem depender dessa classe, não herdar dela.
Correndo o risco de simplificar demais, isso é composição - compondo várias classes para trabalharem juntas. E uma vez que formamos esse hábito, descobrimos que é muito mais flexível, sustentável e testável do que usar herança.
Além das considerações a / /, é preciso considerar também a "profundidade" da herança pela qual seu objeto deve passar. Qualquer coisa além de cinco ou seis níveis de herança profunda pode causar problemas inesperados de conversão e encaixotamento / remoção de caixas e, nesses casos, pode ser aconselhável compor seu objeto.
Quando você tem uma relação is-a entre duas classes (por exemplo, cachorro é um canino), você escolhe a herança.
Por outro lado, quando você tem uma relação adjetiva ou parcial entre duas classes (o aluno tem cursos) ou (cursos de estudos para professores), você escolhe a composição.
Uma maneira simples de entender isso é que a herança deve ser usada quando você precisar que um objeto da sua classe tenha a mesma interface que a classe pai, para que possa ser tratado como um objeto da classe pai (upcasting) . Além disso, as chamadas de função em um objeto de classe derivado permaneceriam iguais em todo o código, mas o método específico a ser chamado seria determinado em tempo de execução (ou seja, a implementação de baixo nível difere, a interface de alto nível permanece a mesma).
A composição deve ser usada quando você não precisa que a nova classe tenha a mesma interface, ou seja, você deseja ocultar certos aspectos da implementação da classe que o usuário dessa classe não precisa conhecer. Portanto, a composição é mais uma forma de apoiar o encapsulamento (ou seja, ocultar a implementação), enquanto a herança é destinada a suportar a abstração (ou seja, fornecer uma representação simplificada de algo, neste caso, a mesma interface para uma variedade de tipos com diferentes componentes internos).
A subtipagem é apropriada e mais poderosa onde os invariantes podem ser enumerados ; caso contrário, use a composição da função para extensibilidade.
Concordo com @Pavel, quando ele diz, há lugares para composição e para herança.
Eu acho que a herança deve ser usada se sua resposta for afirmativa para qualquer uma dessas perguntas.
No entanto, se sua intenção é puramente a de reutilizar o código, a composição provavelmente é uma melhor opção de design.
A herança é um mecanismo poderoso para a reutilização de código. Mas precisa ser usado corretamente. Eu diria que a herança é usada corretamente se a subclasse também for um subtipo da classe pai. Como mencionado acima, o Princípio de Substituição de Liskov é o ponto chave aqui.
Subclasse não é o mesmo que subtipo. Você pode criar subclasses que não são subtipos (e é nesse momento que você deve usar a composição). Para entender o que é um subtipo, vamos dar uma explicação sobre o que é um tipo.
Quando dizemos que o número 5 é do tipo inteiro, estamos declarando que 5 pertence a um conjunto de valores possíveis (como exemplo, veja os valores possíveis para os tipos primitivos do Java). Também estamos afirmando que há um conjunto válido de métodos que posso executar no valor, como adição e subtração. E, finalmente, estamos declarando que há um conjunto de propriedades sempre satisfeitas, por exemplo, se eu adicionar os valores 3 e 5, obterei 8 como resultado.
Para dar outro exemplo, pense nos tipos de dados abstratos, Conjunto de números inteiros e Lista de números inteiros, os valores que eles podem conter são restritos a números inteiros. Ambos suportam um conjunto de métodos, como add (newValue) e size (). E ambos têm propriedades diferentes (classe invariável), Sets não permite duplicados, enquanto List permite duplicados (é claro que existem outras propriedades que ambos satisfazem).
Subtipo também é um tipo, que tem uma relação com outro tipo, chamado tipo pai (ou supertipo). O subtipo deve satisfazer os recursos (valores, métodos e propriedades) do tipo pai. A relação significa que, em qualquer contexto em que o supertipo é esperado, ele pode ser substituído por um subtipo, sem afetar o comportamento da execução. Vamos ver um código para exemplificar o que estou dizendo. Suponha que eu escreva uma Lista de números inteiros (em algum tipo de pseudo-linguagem):
class List {
data = new Array();
Integer size() {
return data.length;
}
add(Integer anInteger) {
data[data.length] = anInteger;
}
}
Então, escrevo o conjunto de números inteiros como uma subclasse da lista de números inteiros:
class Set, inheriting from: List {
add(Integer anInteger) {
if (data.notContains(anInteger)) {
super.add(anInteger);
}
}
}
Nossa classe Conjunto de números inteiros é uma subclasse de Lista de números inteiros, mas não é um subtipo, pois não satisfaz todos os recursos da classe Lista. Os valores e a assinatura dos métodos são satisfeitos, mas as propriedades não. O comportamento do método add (Integer) foi claramente alterado, não preservando as propriedades do tipo pai. Pense do ponto de vista do cliente de suas aulas. Eles podem receber um conjunto de números inteiros onde uma lista de números inteiros é esperada. O cliente pode querer adicionar um valor e obter esse valor na Lista, mesmo que esse valor já exista na Lista. Mas ela não terá esse comportamento se o valor existir. Uma grande surpresa para ela!
Este é um exemplo clássico de uso indevido de herança. Use composição neste caso.
(um fragmento de: use a herança corretamente ).
Uma regra prática que ouvi é que a herança deve ser usada quando se trata de um relacionamento "is-a" e composição quando é um "tem-a". Mesmo com isso, sinto que você deve sempre se inclinar para a composição, pois elimina muita complexidade.
Composição v / s A herança é um assunto amplo. Não existe uma resposta real para o que é melhor, pois acho que tudo depende do design do sistema.
Geralmente, o tipo de relacionamento entre objetos fornece melhores informações para escolher um deles.
Se o tipo de relação é "IS-A", a herança é uma abordagem melhor. caso contrário, o tipo de relação é "HAS-A", então a composição será melhor abordada.
Depende totalmente do relacionamento da entidade.
Embora a Composição seja preferida, gostaria de destacar os profissionais da Herança e os contras da Composição .
Prós da herança:
Estabelece uma relação lógica " IS A" . Se Carro e Caminhão são dois tipos de Veículo (classe base), a classe filho IS A classe base.
ie
Carro é um veículo
Caminhão é um veículo
Com herança, você pode definir / modificar / estender um recurso
Contras de composição:
Por exemplo, se Car contém Veículo e se você precisar obter o preço do Carro , definido em Veículo , seu código será como este
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
Como muitas pessoas disseram, vou começar pela verificação - se existe uma relação "é-a". Se existe, geralmente verifico o seguinte:
Se a classe base pode ser instanciada. Ou seja, se a classe base pode ser não abstrata. Se não puder ser abstrato, geralmente prefiro a composição
Por exemplo: 1. O contador é um funcionário. Mas não usarei herança porque um objeto Employee pode ser instanciado.
Por exemplo, 2. O livro é um item de venda. Um SellingItem não pode ser instanciado - é um conceito abstrato. Por isso, usarei a herança. O SellingItem é uma classe base abstrata (ou interface em C #)
O que você acha dessa abordagem?
Além disso, eu apoio a resposta @anon em Por que usar herança?
O principal motivo para usar a herança não é como uma forma de composição - é para que você possa obter um comportamento polimórfico. Se você não precisa de polimorfismo, provavelmente não deve usar herança.
@MatthieuM. diz em /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
O problema da herança é que ela pode ser usada para dois propósitos ortogonais:
interface (para polimorfismo)
implementação (para reutilização de código)
REFERÊNCIA
Vejo que ninguém mencionou o problema do diamante , que pode surgir com herança.
À primeira vista, se as classes B e C herdam A e ambos substituem o método X, e uma quarta classe D, herdam B e C e não substituem X, qual implementação do XD deve usar?
A Wikipedia oferece uma boa visão geral do tópico discutido nesta pergunta.