Prefere composição sobre herança?

1604

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?

Somente leitura
fonte
3
Veja também que o projeto classe é melhor
maccullt
40
Há um bom artigo sobre essa questão aqui . Minha opinião pessoal é que não existe um princípio "melhor" ou "pior" para o design. Existe um design "apropriado" e "inadequado" para a tarefa concreta. Em outras palavras - eu uso tanto a herança quanto a composição, dependendo da situação. O objetivo é produzir código menor, mais fácil de ler, reutilizar e, eventualmente, estender ainda mais.
M_pGladiator
1
em uma frase, a herança é pública se você possui um método público e o altera, a API publicada. se você tiver composição e o objeto composto tiver sido alterado, não será necessário alterar sua API publicada.
Tomer Ben David

Respostas:

1188

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 .

    • por exemplo, um biplano Cessna irá expor a interface completa de um avião, se não mais. Portanto, é adequado derivar do avião.
  • O TypeB deseja apenas parte do comportamento exposto pelo TypeA? Indica necessidade de composição.

    • Por exemplo, um pássaro pode precisar apenas do comportamento de voar de um avião. Nesse caso, faz sentido extraí-lo como uma interface / classe / ambos e torná-lo um membro de ambas as classes.

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?'

Gishu
fonte
81
O segundo exemplo é extraído do livro Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Eu recomendo esse livro para qualquer pessoa que esteja pesquisando esta questão no Google.
precisa
4
É muito claro, mas pode faltar alguma coisa: "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?" Mas e se isso for verdade, e o TypeB também expuser a interface completa do TypeC? E se o TypeC ainda não tiver sido modelado?
Tristan
4
Você faz alusão ao que eu acho que deveria ser o teste mais básico: "Esse objeto deve ser utilizável por código que espera objetos do (o que seria) o tipo de base". Se a resposta for sim, o objeto deve herdar. Se não, então provavelmente não deveria. Se eu tivesse meus especialistas, os idiomas forneceriam uma palavra-chave para se referir a "esta classe" e forneceriam um meio de definir uma classe que deveria se comportar como outra classe, mas não ser substituível por ela (essa classe teria tudo isso " classe "substituídas por ele mesmo).
Super12 /
22
@ Alexey - o ponto é 'Posso passar um biplano da Cessna para todos os clientes que esperam um avião sem surpreendê-los?'. Se sim, então é provável que você queira herança.
Gishu
9
Na verdade, estou lutando para pensar em alguns exemplos em que a herança teria sido minha resposta; muitas vezes acho que agregação, composição e interfaces resultam em soluções mais elegantes. Muitos dos exemplos acima poderia ser melhor explicados usando essas abordagens ...
Stuart Wakefield
415

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 .

Nick Zalutskiy
fonte
107
Nem sempre é uma abordagem perfeita, é simplesmente uma boa orientação. o princípio de substituição de Liskov é muito mais preciso (falha menos).
Bill K
41
"Meu carro tem um veículo." Se você considerar isso como uma frase separada, não em um contexto de programação, isso não faz absolutamente nenhum sentido. E esse é o objetivo dessa técnica. Se parece estranho, provavelmente está errado.
Nick Zalutskiy
36
@ Nick Claro, mas "Meu carro tem um VehicleBehavior" faz mais sentido (acho que sua classe "Vehicle" poderia se chamar "VehicleBehavior"). Então você não pode basear sua decisão em "tem um" vs "é uma" comparação, você tem que usar LSP, ou você vai cometer erros
Tristan
35
Em vez de "é um" pensar em "se comporta como". A herança é sobre herdar comportamento, não apenas semântica.
ybakos 31/03
4
@ybakos "Comporta-se como" pode ser alcançado através de interfaces sem a necessidade de herança. Da Wikipedia : "Uma implementação de composição sobre herança geralmente começa com a criação de várias interfaces que representam os comportamentos que o sistema deve exibir ... Assim, os comportamentos do sistema são realizados sem herança".
27415 DavidRR
210

Se você entende a diferença, é mais fácil explicar.

Código processual

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.

Herança

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.

Composição

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.

Composição sobre herança

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.

aleemb
fonte
6
Para herança: não há ambiguidade. Você está implementando a classe Manager com base nos requisitos. Portanto, você retornaria o "Gerente de Operações" se esses requisitos fossem especificados, caso contrário, você usaria apenas a implementação da classe base. Além disso, você pode tornar Person uma classe abstrata e, assim, garantir que as classes downstream implementem uma propriedade Title.
Raj Rao
68
É importante lembrar que se pode dizer "Composição sobre herança", mas isso não significa "Composição sempre sobre Herança". "É um" significa herança e leva à reutilização de código. Empregado é uma Pessoa (Empregado não tem uma pessoa).
Raj Rao
36
O exemplo é confuso. O funcionário é uma pessoa, portanto, ele deve usar herança. Você não deve usar composição para este exemplo, porque é um relacionamento incorreto no modelo de domínio, mesmo que tecnicamente você possa declará-lo no código.
22713 Michael Freidgeim
15
Não concordo com este exemplo. Um Funcionário é uma Pessoa, que é um caso de uso adequado da herança. Eu também acho que o "problema" da redefinição do campo Título não faz sentido. O fato de Employee.Title obscurecer Person.Title é um sinal de má programação. Afinal, são "Sr." e "Gerente de operações" realmente se referindo ao mesmo aspecto de uma pessoa (minúscula)? Renomearia Employee.Title e, portanto, seria capaz de referenciar os atributos Title e JobTitle de um Employee, os quais fazem sentido na vida real. Além disso, não há nenhuma razão para Manager (continuação ...)
Radon Rosborough
9
(... continuação) para herdar da Pessoa e do Funcionário - afinal, o Funcionário já herda da Pessoa. Em modelos mais complexos, nos quais uma pessoa pode ser um gerente e um agente, é verdade que várias heranças podem ser usadas (com cuidado!), Mas seria preferível em muitos ambientes ter uma classe de função abstrata a partir da qual o gerente (contém funcionários ele administra) e o Agente (contém contratos e outras informações) herda. Então, um funcionário é uma pessoa que possui várias funções. Assim, a composição e a herança são usadas corretamente.
Radon Rosborough 10/11
142

Com todos os benefícios inegáveis ​​fornecidos pela herança, eis algumas de suas desvantagens.

Desvantagens da herança:

  1. Você não pode alterar a implementação herdada das superclasses no tempo de execução (obviamente porque a herança é definida no tempo de compilação).
  2. A herança expõe uma subclasse aos detalhes de sua implementação de classe pai, é por isso que muitas vezes se diz que a herança quebra o encapsulamento (no sentido de que você realmente precisa se concentrar nas interfaces, não na implementação, portanto, nem sempre é preferível a reutilização pela subclasse).
  3. O acoplamento rígido fornecido pela herança torna a implementação de uma subclasse muito ligada à implementação de uma superclasse de que qualquer alteração na implementação pai forçará a subclasse a mudar.
  4. A reutilização excessiva por subclassificação pode tornar a pilha de herança muito profunda e muito confusa também.

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.

Galilyou
fonte
5
Esta é uma das melhores respostas, na minha opinião - acrescentarei a isso que tentar repensar seus problemas em termos de composição, na minha experiência, tende a levar a classes menores, mais simples, mais independentes e mais reutilizáveis , com um escopo de responsabilidade mais claro, menor e mais focado. Geralmente, isso significa que há menos necessidade de coisas como injeção de dependência ou zombaria (em testes), pois os componentes menores geralmente são capazes de se sustentar sozinhos. Apenas minha experiência. YMMV :-)
mindplay.dk 17/01
3
O último parágrafo deste post realmente me agradou. Obrigado.
Salx 25/05
87

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.

Tim Howland
fonte
81

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.

Pavel Feldman
fonte
5
A ferramenta certa para o trabalho certo. Um martelo pode ser melhor em bater coisas do que uma chave inglesa, mas isso não significa que se deva ver uma chave inglesa como "um martelo inferior". A herança pode ser útil quando as coisas adicionadas à subclasse são necessárias para que o objeto se comporte como um objeto de superclasse. Por exemplo, considere uma classe base InternalCombustionEnginecom 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 InternalCombustionEnginefará com que as velas sejam usadas.
Supercat 29/10
61

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

herança é provavelmente a pior forma de acoplamento que você pode ter

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.

Mike Valenty
fonte
A herança não é boa ou ruim, é apenas um caso especial de Composição. Onde, de fato, a subclasse está implementando uma funcionalidade semelhante à superclasse. Se a subclasse proposta não estiver sendo reimplementada, mas apenas usando a funcionalidade da superclasse, você utilizou a Herança incorretamente. Esse é o erro do programador, não uma reflexão sobre a herança.
iPherian
42

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.

dance2die
fonte
10
+1 Descobri cada vez menos que a herança funciona na maioria das situações. Eu prefiro interfaces compartilhadas / herdadas e composição de objetos .... ou é chamado de agregação? Não me pergunte, eu tenho um diploma de EE !!
Kenny
Acredito que este é o cenário mais comum em que a "composição sobre herança" se aplica, uma vez que ambas podem se encaixar na teoria. Por exemplo, em um sistema de marketing, você pode ter o conceito de a Client. Então, um novo conceito de um PreferredClientaparece mais tarde. Deve PreferredClientherdar 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, Accounttalvez?
Plalx 31/08/2015
Ao invés de ter vários tipos de Clientaulas, talvez haja apenas um que encapsula o conceito de Accountque poderia ser um StandardAccountou uma PreferredAccount...
plalx
40

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

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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 .

  5. A composição tem o benefício da programação orientada a combinadores , ou seja, funciona de maneira semelhante ao padrão composto .

  6. A composição segue imediatamente a programação para uma interface .

  7. 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.

lcn
fonte
34

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.

Mecki
fonte
"Muitas pessoas pensam que a herança representa nosso mundo real muito bem, mas essa não é a verdade." Tanto isso! Ao contrário de praticamente todos os tutoriais de programação do mundo, modelar objetos do mundo real como cadeias de herança é provavelmente uma má idéia a longo prazo. Você deve usar herança somente quando houver um relacionamento incrivelmente óbvio, inato e simples. Como TextFileé um File.
neonblitzer
25

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

Peter Tseng
fonte
25

Por que preferir composição a herança?

Veja outras respostas.

Quando você pode usar herança?

Costuma-se dizer que uma classe Barpode herdar uma classe Fooquando a seguinte frase for verdadeira:

  1. um bar é um foo

Infelizmente, o teste acima sozinho não é confiável. Use o seguinte:

  1. um bar é um foo, E
  2. as barras podem fazer tudo o que os foos podem fazer.

Os primeiros testes garante que todos os getters de Foofazer sentido no Bar(= propriedades partilhadas), enquanto o segundo teste garante que todos os formadores de Foofazer 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.

Quando você deve usar herança?

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 Widgetem 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.

Princípio da substituição de Liskov

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".

Boris Dalstein
fonte
Os cenários de elipse circular e retângulo quadrado são maus exemplos. As subclasses são invariavelmente mais complexas que a superclasse, portanto o problema é artificial. Esse problema é resolvido invertendo o relacionamento. Uma elipse deriva de um círculo e um retângulo de um quadrado. É extremamente bobo usar a composição nesses cenários.
Fuzzy Logic
@FuzzyLogic Concordo, mas na verdade, meu post nunca defende o uso de composição neste caso. Eu apenas disse que o problema da elipse circular é um ótimo exemplo de por que "is-a" não é um bom teste para concluir que o Circle deve derivar da Ellipse. Depois de concluirmos que, na verdade, o Circle não deve derivar do Ellipse devido à violação do LSP, as opções possíveis são inverter o relacionamento, usar a composição, usar classes de modelo ou usar um design mais complexo, envolvendo classes adicionais ou funções auxiliares, etc ... e obviamente a decisão deve ser tomada caso a caso.
Boris Dalstein
1
@FuzzyLogic E se você está curioso sobre o que eu advogaria no caso específico da Circle-Ellipse: eu advogaria a não implementação da classe Circle. O problema de inverter o relacionamento é que ele também viola o LSP: imagine a função 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.
Boris Dalstein
computeArea(Circle *c) { return pi * width * height / 4.0; }Agora é genérico.
Fuzzy Logic
2
@FuzzyLogic Não concordo: você entende que isso significa que a classe Circle antecipou a existência da classe derivada Ellipse e, portanto, forneceu 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.
Boris Dalstein
19

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.

yukondude
fonte
15

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).

egaga
fonte
3
Vale a pena notar que a herança não é a única maneira de alcançar o polimorfismo. O Padrão Decorador fornece a aparência de polimorfismo através da composição.
BitMask777
1
@ BitMask777: Polimorfismo de subtipo é apenas um tipo de polimorfismo, outro seria polimorfismo paramétrico, você não precisa de herança para isso. Mais importante ainda: quando se fala em herança, significa herança de classe; .ie você pode ter um polimorfismo de subtipo tendo uma interface comum para várias classes e não obtém os problemas de herança.
egaga 23/09/12
2
@engaga: interpretei seu comentário Inheritance is sometimes useful... That way you can have polymorphismcomo 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.
BitMask777
15

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 Engineexpõe um mutador para alterar suas
propriedades ou redesenho Aircraftcomo:

Class Aircraft {
  var wings;
  var engine;
}

Agora, também posso substituir meu motor em tempo real.

simplfuzz
fonte
Sua postagem traz um ponto que eu não havia considerado antes - para continuar com uma analogia de objetos mecânicos com várias partes, em algo como uma arma de fogo, geralmente há uma parte marcada com um número de série, cujo número de série é considerado o da arma de fogo como um todo (para uma pistola, normalmente seria a armação). Pode-se substituir todas as outras peças e ainda ter a mesma arma de fogo, mas se a armação quebrar e precisar ser substituída, o resultado da montagem de uma nova armação com todas as outras peças da arma original seria uma arma nova. Note-se que ... #
22612
... o fato de várias partes de uma arma poderem ter números de série marcados nelas não significa que uma arma possa ter múltiplas identidades. Somente o número de série no quadro identifica a pistola; o número de série de qualquer outra parte identifica com qual pistola essas peças foram fabricadas para serem montadas, que podem não ser a pistola na qual elas são montadas em um determinado momento.
Supercat 19/08
7

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.

Anzurio
fonte
7

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:

  1. Você tem muitas classes com a mesma interface e deseja economizar tempo escrevendo-as
  2. Você precisa usar a mesma classe base para cada objeto
  3. Você precisa modificar as variáveis ​​privadas, que não podem ser públicas em nenhum caso

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.

Tero Tolonen
fonte
7

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:

  • Descobrimos que existem outras funções que queremos que nossas aulas tenham. Se a maneira como adicionamos funcionalidade às classes é por herança, temos que decidir - a adicionamos à classe base existente, mesmo que nem toda classe que herda dela precise dessa funcionalidade? Criamos outra classe base? Mas e as classes que já herdam da outra classe base?
  • Descobrimos que, para apenas uma das classes que herda nossa classe base, queremos que a classe base se comporte de maneira um pouco diferente. Então agora vamos voltar e mexer com nossa classe base, talvez adicionando alguns métodos virtuais, ou pior ainda, algum código que diga: "Se eu sou herdado do tipo A, faça isso, mas se for herdado do tipo B, faça isso . " Isso é ruim por muitas razões. Uma é que toda vez que alteramos a classe base, estamos efetivamente alterando todas as classes herdadas. Então, estamos realmente mudando as classes A, B, C e D porque precisamos de um comportamento ligeiramente diferente na classe A. Por mais cuidadosos que pensemos que somos, podemos quebrar uma dessas classes por razões que não têm nada a ver com elas. Aulas.
  • Podemos saber por que decidimos herdar todas essas classes uma da outra, mas talvez não (provavelmente não faça) faça sentido para alguém que precisa manter nosso código. Podemos forçá-los a uma escolha difícil - faço algo realmente feio e bagunçado para fazer a alteração necessária (veja o ponto anterior) ou apenas reescrevo um monte disso.

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.

Scott Hannen
fonte
O fato de as classes não poderem usar várias classes base não é uma reflexão ruim sobre Herança, mas uma reflexão ruim sobre a falta de capacidade de um idioma específico.
iPherian
Desde que escrevi esta resposta, li este post do "Tio Bob", que aborda essa falta de capacidade. Eu nunca usei uma linguagem que permita herança múltipla. Mas, olhando para trás, a pergunta está marcada como "agnóstico de idioma" e minha resposta assume C #. Eu preciso ampliar meus horizontes.
21717 Scott Schaeffler em:
6

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.

Jon Limjap
fonte
6

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.

Amir Aslam
fonte
Você disse herança e herança. você não quer dizer herança e composição?
trevorKirkby
Não você não. Você também pode definir uma interface canina e permitir que todos os cães a implementem e você terá mais código SOLID.
Markus
5

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).

YS
fonte
+1 para menção de interface. Uso essa abordagem frequentemente para ocultar classes existentes e tornar minha nova classe adequadamente testável por unidade, zombando do objeto usado para composição. Isso requer que o proprietário do novo objeto passe para ele a classe pai candidata.
Kell
4

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.

  • Sua turma faz parte de uma estrutura que se beneficia do polimorfismo? Por exemplo, se você tivesse uma classe Shape, que declara um método chamado draw (), claramente precisamos que as classes Circle e Square sejam subclasses de Shape, para que suas classes clientes dependam de Shape e não de subclasses específicas.
  • Sua turma precisa reutilizar interações de alto nível definidas em outra turma? O padrão de design do método de modelo seria impossível de implementar sem herança. Eu acredito que todas as estruturas extensíveis usam esse padrão.

No entanto, se sua intenção é puramente a de reutilizar o código, a composição provavelmente é uma melhor opção de design.

Parag
fonte
4

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 ).

Enrique Molinari
fonte
3

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.


fonte
2

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.

Shami Qureshi
fonte
2

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:

  1. 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

  2. Com herança, você pode definir / modificar / estender um recurso

    1. A classe base não fornece implementação e a subclasse precisa substituir o método completo (resumo) => Você pode implementar um contrato
    2. A classe base fornece implementação padrão e a subclasse pode mudar o comportamento => Você pode redefinir o contrato
    3. A subclasse adiciona extensão à implementação da classe base chamando super.methodName () como primeira instrução => Você pode estender um contrato
    4. A classe base define a estrutura do algoritmo e a subclasse substituirá uma parte do algoritmo => Você pode implementar o Template_method sem alterar o esqueleto da classe base

Contras de composição:

  1. Na herança, a subclasse pode chamar diretamente o método da classe base, mesmo que não esteja implementando o método da classe base por causa da relação IS A. Se você usar composição, precisará adicionar métodos na classe de contêiner para expor a API da classe contida

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();
     }
} 
Ravindra babu
fonte
Eu acho que não responder à pergunta
almanegra
Você pode repensar a questão do OP. Abordei: Que trade-offs existem para cada abordagem?
Ravindra Babu
Como você mencionou, você falar apenas sobre "prós de herança e contras de Composição", e não as implicações de cada abordagem ou os casos em que você deve usar um sobre o outro
almanegra
prós e contras oferece um compromisso, já que os prós da herança são contras de composição e os contras da composição são prós da herança.
Ravindra Babu
1

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

  1. Qual design de classe é melhor?
  2. Herança vs. Agregação
LCJ
fonte
1
Não sei por que 'a classe base é abstrata?' figuras na discussão .. o LSP: todas as funções que operam em Dogs funcionam se objetos Poodle forem passados? Se sim, então Poodle is pode ser substituído por Dog e, portanto, pode herdar de Dog.
Gishu 01/08/2012
@Gishu Thanks. Definitivamente vou olhar para o LSP. Mas antes disso, você pode fornecer um "exemplo em que a herança é apropriada, na qual a classe base não pode ser abstrata". O que penso é que a herança é aplicável apenas se a classe base for abstrata. Se a classe base precisar ser instanciada separadamente, não use herança. Ou seja, mesmo que o contador seja um funcionário, não use herança.
LCJ
1
ultimamente tenho lido o WCF. Um exemplo na estrutura .net é SynchronizationContext (base + pode ser instanciada), que as filas funcionam em um thread ThreadPool. As derivações incluem WinFormsSyncContext (fila no thread da interface do usuário) e DispatcherSyncContext (fila no Dispatcher do WPF) #
584
@Gishu Thanks. No entanto, seria mais útil se você pudesse fornecer um cenário com base no domínio do banco, domínio do RH, domínio do varejo ou qualquer outro domínio popular.
LCJ
1
Desculpa. Não estou familiarizado com esses domínios. Outro exemplo, se o anterior era muito obtuso, é a classe Control no Winforms / WPF. O controle básico / genérico pode ser instanciado. As derivações incluem caixas de listagem, caixas de texto etc. Agora que penso nisso, o padrão Decorator Design é um bom exemplo IMHO e também útil. O decorador deriva do objeto não abstrato que deseja quebrar / decorar.
Gishu 2/08/12
1

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.

Veverke
fonte
1
B e C D herda não A. Se assim for, então seria utilizar a implementação de X que é de classe A.
fabricio
1
@ fabricio: obrigado, editei o texto. A propósito, esse cenário não pode ocorrer em idiomas que não permitem herança de várias classes, estou certo?
Veverke 14/05
sim você está certo .. e eu nunca trabalhei com um que permite herança múltipla (como por exemplo no problema diamante) ..
fabricio