“Preferir composição sobre herança” - é o único motivo para se defender contra alterações de assinatura?


13

Esta página defende composição sobre herança com o seguinte argumento (reformulado em minhas palavras):

Uma alteração na assinatura de um método da superclasse (que não foi substituída na subclasse) causa alterações adicionais em muitos lugares quando usamos Herança. No entanto, quando usamos o Composition, a alteração adicional necessária ocorre apenas em um único local: a subclasse.

Essa é realmente a única razão para favorecer a composição sobre a herança? Porque, se esse for o caso, esse problema pode ser facilmente aliviado através da aplicação de um estilo de codificação que advoga a substituição de todos os métodos da superclasse, mesmo que a subclasse não altere a implementação (ou seja, colocando substituições fictícias na subclasse). Estou faltando alguma coisa aqui?



1
veja também: Discuta este $ {blog}
gnat

2
A citação não diz que é "a única" razão.
Tulains Córdova

3
A composição é quase sempre melhor, porque a herança geralmente adiciona complexidade, acoplamento, leitura de código e reduz a flexibilidade. Mas há exceções notáveis, às vezes uma classe base ou duas não dói. É por isso que ' prefere ' em vez de ' sempre '.
Mark Rogers

Respostas:


27

Eu gosto de analogias, então aqui está uma: você já viu uma daquelas TVs que tinham um videocassete embutido? Que tal aquele com um videocassete e um DVD player? Ou um que possua Blu-ray, DVD e videocassete. Ah, e agora todo mundo está transmitindo, então precisamos criar um novo aparelho de TV ...

Provavelmente, se você possui um aparelho de TV, não possui um como o acima. Provavelmente, a maioria nunca existiu. Você provavelmente tem um monitor ou conjunto com inúmeras entradas, para que não precise de um novo conjunto sempre que houver um novo tipo de dispositivo de entrada.

A herança é como a TV com o videocassete embutido. Se você tem 3 tipos diferentes de comportamentos que deseja usar juntos e cada um possui 2 opções de implementação, você precisa de 8 classes diferentes para representar todas elas. À medida que você adiciona mais opções, os números explodem. Se você usar a composição, evite esse problema combinatório e o design tenderá a ser mais extensível.


6
Havia muitas TVs com videocassete e DVD embutidos no final dos anos 90 e início dos anos 2000.
JAB

A @JAB e muitas (a maioria?) TVs vendidas hoje oferecem suporte a streaming, nesse caso.
Casey

4
@JAB E nenhum deles pode ter o seu leitor de DVD vendidos para ajudar a comprar um leitor de Blu-ray
Alexander - Reintegrar Monica

1
@JAB Certo. A declaração "Você já viu uma daquelas TVs que tinham um videocassete embutido?" implica que eles existem.
JimmyJames 24/05

4
@ Casey Porque, obviamente, nunca haverá outra tecnologia que substitua essa, certo? Aposto que qualquer uma dessas TVs também suporta entradas de dispositivos externos, caso alguém queira usar outra coisa sem comprar uma nova TV. Ou melhor, poucos compradores instruídos estão dispostos a comprar uma TV que carece de tais insumos. Minha premissa não é que essas abordagens não existam e nem eu afirmaria que a herança não existe. O ponto é que essas abordagens são inerentemente menos flexíveis que a composição.
JimmyJames

4

Não não é. Na minha experiência, composição sobre herança é resultado de pensar no seu software como um monte de objetos com comportamentos que se comunicam / objetos que prestam serviços um ao outro em vez de classes e dados .

Dessa forma, você tem alguns objetos que fornecem serviços. Você os usa como blocos de construção (muito parecidos com o Lego) para permitir outro comportamento (por exemplo, um UserRegistrationService usa os serviços fornecidos por um EmailerService e um UserRepository ).

Ao construir sistemas maiores com essa abordagem, naturalmente usei herança em muito poucos casos. No final do dia, a herança é uma ferramenta muito poderosa que deve ser usada com cautela e somente quando apropriado.


Eu vejo a mentalidade Java aqui. OOP é sobre os dados que estão sendo empacotados junto com as funções que atuam nele - o que você está descrevendo é melhor denominado "módulos". Você está certo em que evitar a herança é uma boa idéia ao redirecionar objetos como módulos, mas acho perturbador que você pareça recomendar isso como a maneira preferida de usar objetos.
Brilliand

1
@ Brilliand Se você soubesse o quanto o código java é escrito no estilo que você descreve e o quanto isso é um horror ... Smalltalk introduziu o termo OOP e foi projetado em torno de objetos que recebem mensagens. : "A computação deve ser vista como um recurso intrínseco de objetos que podem ser invocados uniformemente enviando mensagens." Não há menção de dados e funções operando nele. Desculpe, mas na minha opinião você está gravemente enganado. Se fosse apenas sobre dados e funções, seria possível continuar feliz escrevendo estruturas.
Marstato 24/05

@Tsar infelizmente, apesar de Java suporta métodos estáticos, a cultura Java não
k_g

@Brilliand Quem se importa com o que é "OOP"? O que importa é como você pode usá-lo e os resultados do uso dessa maneira.
User253751

1
Após discussão com @Tsar: classes Java não tem que ser instanciado e, na verdade, classes como UserRegistrationService e EmailService geralmente não deveria ser instanciado - aulas sem dados associados funcionam melhor como classes estáticas (e, como tal, são irrelevantes para o original questão). Excluirei alguns dos meus comentários anteriores, mas minha crítica original à resposta de marsato permanece.
Brilliand

4

"Preferir composição sobre herança" é apenas uma boa heurística

Você deve considerar o contexto, pois essa não é uma regra universal. Não pense que nunca use herança quando puder fazer composição. Se fosse esse o caso, você poderia corrigi-lo banindo a herança.

Espero deixar claro esse ponto ao longo deste post.

Não tentarei defender os méritos da composição por si só. O que considero fora de tópico. Em vez disso, falarei sobre algumas situações em que o desenvolvedor pode considerar a herança que seria melhor usar a composição. Nessa nota, a herança tem seus próprios méritos, os quais também considero fora de tópico.


Exemplo de veículo

Estou escrevendo sobre desenvolvedores que tentam fazer as coisas da maneira idiota, para fins narrativos

Vamos para uma variante dos exemplos clássicos que alguns cursos OOP usar ... Nós temos uma Vehicleclasse, então derivamos Car, Airplane, Balloone Ship.

Nota : Se você precisar fundamentar este exemplo, finja que estes são tipos de objetos em um videogame.

Então, Care Airplanepode ter algum código comum, porque ambos podem rolar sobre rodas em terra. Os desenvolvedores podem considerar a criação de uma classe intermediária na cadeia de herança para isso. No entanto, na verdade, também há algum código compartilhado entre Airplanee Balloon. Eles poderiam considerar criar outra classe intermediária na cadeia de herança para isso.

Assim, o desenvolvedor procuraria por herança múltipla. No ponto em que os desenvolvedores estão procurando por herança múltipla, o design já deu errado.

É melhor modelar esse comportamento como interfaces e composição, para que possamos reutilizá-lo sem precisar ter herança de várias classes. Se os desenvolvedores, por exemplo, criarem a FlyingVehiculeclasse. Eles estariam dizendo que Airplaneé uma FlyingVehicule(herança de classe), mas poderíamos dizer que Airplanetem um Flyingcomponente (composição) e Airplaneé uma IFlyingVehicule(herança de interface).

Usando interfaces, se necessário, podemos ter herança múltipla (de interfaces). Além disso, você não está acoplando a uma implementação específica. Aumentar a reutilização e a testabilidade do seu código.

Lembre-se de que a herança é uma ferramenta para o polimorfismo. Além disso, o polimorfismo é uma ferramenta para reutilização. Se você pode aumentar a reutilização do seu código usando a composição, faça-o. Se você não tiver certeza de que a composição fornece ou não melhor capacidade de reutilização, "Preferir composição à herança" é uma boa heurística.

Tudo isso sem mencionar Amphibious.

De fato, podemos não precisar de coisas que decolam. Stephen Hurn tem um exemplo mais eloquente em seus artigos "Favor da composição sobre a herança", parte 1 e parte 2 .


Substitutibilidade e Encapsulamento

Deve Aherdar ou compor B?

Se Aé uma especialização Bdisso que deve cumprir o princípio de substituição de Liskov , a herança é viável, até desejável. Se houver situações em que Anão há uma substituição válida B, não devemos usar herança.

Podemos estar interessados ​​na composição como uma forma de programação defensiva, para defender a classe derivada . Em particular, quando você começar a usar Bpara outros fins, pode haver pressão para alterar ou estendê-lo para que seja mais adequado para esses fins. Se houver o risco de Bexpor métodos que possam resultar em um estado inválido A, devemos usar a composição em vez da herança. Mesmo se somos os autores de ambos Be A, é uma coisa a menos com que se preocupar, pois a composição facilita a reutilização B.

Podemos até argumentar que, se houver recursos Bque Anão sejam necessários (e não sabemos se esses recursos podem resultar em um estado inválido para A, seja na presente implementação ou no futuro), é uma boa ideia usar a composição em vez de herança.

A composição também tem as vantagens de permitir a troca de implementações e facilitar a zombaria.

Nota : há situações em que queremos usar a composição, apesar da substituição ser válida. Arquivamos essa substituibilidade usando interfaces ou classes abstratas (qual usar quando é outro tópico) e depois usamos a composição com injeção de dependência da implementação real.

Finalmente, é claro, há o argumento de que devemos usar a composição para defender a classe pai, porque a herança quebra o encapsulamento da classe pai:

A herança expõe uma subclasse a detalhes da implementação de seus pais, é comum dizer que 'a herança quebra o encapsulamento'

- Padrões de projeto: elementos de software orientado a objetos reutilizáveis, Gang of Four

Bem, essa é uma classe pai mal projetada. É por isso que você deve:

Projete para herança ou proíba-a.

- Java eficaz, Josh Bloch


Problema de ioiô

Outro caso em que a composição ajuda é o problema do ioiô . Esta é uma citação da Wikipedia:

No desenvolvimento de software, o problema do ioiô é um antipadrão que ocorre quando um programador precisa ler e entender um programa cujo gráfico de herança é tão longo e complicado que o programador precisa continuar alternando entre muitas definições diferentes de classe para seguir o fluxo de controle do programa.

Você pode resolver, por exemplo: Sua classe Cnão herdará da classe B. Em vez disso, sua classe Cterá um membro do tipo A, que pode ou não ser (ou ter) um objeto do tipo B. Dessa forma, você não estará programando com os detalhes de implementação B, mas com o contrato que a interface (de) Aoferece.


Exemplos de contadores

Muitas estruturas favorecem a herança sobre a composição (que é o oposto do que estamos discutindo). O desenvolvedor pode fazer isso porque eles colocam muito trabalho em sua classe base que implementá-lo com composição aumentaria o tamanho do código do cliente. Às vezes, isso ocorre devido a limitações do idioma.

Por exemplo, uma estrutura ORM do PHP pode criar uma classe base que usa métodos mágicos para permitir a gravação de código como se o objeto tivesse propriedades reais. Em vez disso, o código manipulado pelos métodos mágicos vai para o banco de dados, consulta o campo solicitado específico (talvez o armazene em cache para solicitação futura) e o retorna. Fazer isso com composição exigiria que o cliente criasse propriedades para cada campo ou escrevesse alguma versão do código dos métodos mágicos.

Adendo : Existem outras maneiras de permitir a extensão de objetos ORM. Portanto, não acho que a herança seja necessária neste caso. É mais barato.

Por outro exemplo, um mecanismo de videogame pode criar uma classe base que usa código nativo, dependendo da plataforma de destino para fazer renderização em 3D e manipulação de eventos. Este código é complexo e específico da plataforma. Seria caro e passível de erro para o usuário desenvolvedor do mecanismo lidar com esse código, de fato, isso faz parte do motivo do uso do mecanismo.

Além disso, sem a parte de renderização 3D, é quantas estruturas de widget funcionam. Isso evita que você se preocupe em lidar com mensagens do sistema operacional ... de fato, em muitos idiomas, você não pode escrever esse código sem alguma forma de restrição nativa. Além disso, se você o fizesse, aumentaria sua portabilidade. Em vez disso, com herança, desde que o desenvolvedor não quebre a compatibilidade (demais); você poderá portar facilmente seu código para quaisquer novas plataformas suportadas no futuro.

Além disso, considere que muitas vezes queremos substituir apenas alguns métodos e deixar todo o resto com as implementações padrão. Se estivéssemos usando a composição, teríamos que criar todos esses métodos, mesmo que apenas para delegar ao objeto empacotado.

Por esse argumento, há um ponto em que a composição pode ser pior para manutenção do que herança (quando a classe base é muito complexa). No entanto, lembre-se de que a manutenção da herança pode ser pior do que a da composição (quando a árvore de herança é muito complexa), a qual me refiro no problema do ioiô.

Nos exemplos apresentados, os desenvolvedores raramente pretendem reutilizar o código gerado por herança em outros projetos. Isso mitiga a reutilização diminuída do uso de herança em vez de composição. Além disso, usando a herança, os desenvolvedores da estrutura podem fornecer muito código fácil de usar e fácil de descobrir.


Pensamentos finais

Como você pode ver, a composição tem alguma vantagem sobre a herança em algumas situações, nem sempre. É importante considerar o contexto e os diferentes fatores envolvidos (como reutilização, manutenção, testabilidade, etc.) para tomar a decisão. Voltando ao primeiro ponto: "Preferir composição sobre herança" é apenas uma boa heurística.

Você também pode perceber que muitas das situações que descrevo podem ser resolvidas com Traits ou Mixins até certo ponto. Infelizmente, esses recursos não são comuns na grande lista de idiomas e geralmente vêm com algum custo de desempenho. Felizmente, seus métodos populares Extension Extension e Default Implementations atenuam algumas situações.

Tenho um post recente em que falo sobre algumas das vantagens das interfaces, por que exigimos interfaces entre UI, negócios e acesso a dados em C # . Ajuda a dissociar e facilita a reutilização e a testabilidade, você pode estar interessado.


A estrutura do .NET usa vários tipos de fluxos herdados para fazer seu trabalho relacionado ao fluxo. Funciona incrivelmente bem e é super fácil de usar e estender. Seu exemplo não é muito bom ...
T. Sar - Restabelece Monica

1
@TSar "é super fácil de usar e estender" Você já tentou colocar o fluxo de saída padrão no Debug? Essa tem sido a minha única experiência tentando estender seus fluxos. Nao foi facil. Foi um pesadelo que acabou comigo substituindo explicitamente todos os métodos da classe. Extremamente repetitivo. E se você olhar para o código-fonte .NET, substituir tudo explicitamente é basicamente como eles fazem isso. Portanto, não estou convencido de que seja tão fácil de estender.
Jpmc26

Ler e escrever uma estrutura a partir de um fluxo é melhor feito com funções livres ou multimétodos.
User253751

@ jpmc26 Lamento que sua experiência não tenha sido muito boa. No entanto, não tive problemas em usar ou implementar coisas relacionadas ao fluxo para coisas diferentes.
T. Sar - Restabelece Monica

3

Normalmente, a idéia principal da composição por herança é fornecer maior flexibilidade de design e não reduzir a quantidade de código que você precisa alterar para propagar uma alteração (que considero questionável).

O exemplo na wikipedia dá um exemplo melhor do poder de sua abordagem:

Definindo uma interface base para cada "peça de composição", você pode facilmente criar vários "objetos compostos" com uma variedade que pode ser difícil de alcançar apenas com herança.


Portanto, esta seção fornece um exemplo de composição, certo? Estou perguntando, porque não é óbvio no artigo.
precisa

1
Sim, "Composição sobre herança" não significa que você não tem interfaces. Se você olhar para o objeto Player, poderá ver que ele se encaixa perfeitamente em uma hierarquia, mas o código (desculpe a brutalidade) não é proveniente de herança, mas a partir da composição com um pouco de classe que faz o trabalho
lbenini

2

A linha: "Preferir composição sobre herança" não passa de uma cutucada na direção certa. Isso não vai te salvar da sua própria estupidez e, na verdade, lhe dá uma chance de tornar as coisas muito piores. Isso porque há muito poder aqui.

A principal razão pela qual isso ainda precisa ser dito é porque os programadores são preguiçosos. Tão preguiçosos que tomarão qualquer solução que signifique menos digitação no teclado. Esse é o principal ponto de venda da herança. Eu digito menos hoje e alguém se ferra amanhã. Então o que, eu quero ir para casa.

No dia seguinte, o chefe insiste em usar composição. Não explica o porquê, mas está insistindo nisso. Portanto, tomo uma instância do que eu estava herdando, exponho uma interface idêntica ao que eu estava herdando e implemento essa interface delegando todo o trabalho àquilo que eu estava herdando. É tudo digitação com morte cerebral que poderia ter sido feita com uma ferramenta de refatoração e parece inútil para mim, mas o chefe queria, então eu o faço.

No dia seguinte, pergunto se era isso que se queria. O que você acha que o chefe diz?

A menos que houvesse alguma necessidade de poder alterar dinamicamente (em tempo de execução) o que estava sendo herdado (veja o padrão de estado), isso era uma enorme perda de tempo. Como o chefe pode dizer isso e ainda ser a favor da composição sobre a herança?

Além disso, não fazer nada para impedir a quebra devido a alterações na assinatura do método, isso perde totalmente a maior vantagem da composição. Ao contrário da herança, você é livre para criar uma interface diferente . Mais um apropriado para como será usado neste nível. Você sabe, abstração!

Existem algumas pequenas vantagens em substituir automaticamente a herança pela composição e delegação (e algumas desvantagens), mas manter o cérebro desligado durante esse processo está perdendo uma grande oportunidade.

Indirecionar é muito poderoso. Use com sabedoria.


0

Aqui está outro motivo: em muitas linguagens OO (estou pensando em linguagens como C ++, C # ou Java aqui), não há como alterar a classe de um objeto depois que você o criou.

Suponha que estamos criando um banco de dados de funcionários. Temos uma classe abstrata de funcionários básica e começamos a derivar novas classes para funções específicas - engenheiro, segurança, motorista de caminhão e assim por diante. Cada classe tem um comportamento especializado para esse papel. Tudo é bom.

Então, um dia, um engenheiro é promovido a gerente de engenharia. Você não pode reclassificar um engenheiro existente. Em vez disso, você deve escrever uma função que crie um gerente de engenharia, copiando todos os dados do engenheiro, excluindo o engenheiro do banco de dados e adicionando o novo gerente de engenharia. E você precisa escrever uma função para cada possível mudança de função.

Pior ainda, suponha que um motorista de caminhão fique de licença médica de longo prazo e um segurança se ofereça para fazer o serviço de caminhão em tempo parcial para preencher. Agora você precisa inventar uma nova classe de segurança e motorista de caminhão apenas para eles.

Facilita muito a vida a criação de uma classe Employee concreta e a trata como um contêiner no qual você pode adicionar propriedades, como a função do trabalho.


Ou você cria uma classe Employee com um ponteiro para uma Lista de funções e cada trabalho real estende a Função. Seu exemplo pode ser feito para usar a herança de uma maneira muito boa e sensata, sem muito trabalho.
T. Sar - Restabelece Monica

0

A herança fornece duas coisas:

1) Reutilização de código: pode ser realizado facilmente através da composição. E, de fato, é melhor alcançado através da composição, pois preserva melhor o encapsulamento.

2) Polimorfismo: Pode ser realizado através de "interfaces" (classes em que todos os métodos são totalmente virtuais).

Um argumento forte para o uso de herança sem interface, é quando o número de métodos necessários é grande e sua subclasse deseja apenas alterar um pequeno subconjunto deles. No entanto, em geral, sua interface deve ter pegadas muito pequenas se você se inscrever no princípio de "responsabilidade única" (você deve), portanto, esses casos devem ser raros.

Ter o estilo de codificação que exige a substituição de todos os métodos da classe pai não evita escrever um grande número de métodos de passagem, então por que não reutilizar o código através da composição e obter o benefício adicional do encapsulamento? Além disso, se a superclasse fosse estendida adicionando outro método, o estilo de codificação exigiria a adição desse método a todas as subclasses (e há uma boa chance de estarmos violando a "segregação de interface" ). Esse problema cresce rapidamente quando você começa a ter vários níveis de herança.

Finalmente, em muitos idiomas o teste de unidade de objetos baseados em "composição" é muito mais fácil do que os objetos baseados em "herança" de teste de unidade, substituindo o objeto membro por um objeto mock / test / dummy.


0

Essa é realmente a única razão para favorecer a composição sobre a herança?

Não.

Há muitas razões para favorecer a composição sobre a herança. Não existe uma única resposta correta. Mas vou mostrar outro motivo para favorecer a composição sobre a herança:

Favorecer a composição sobre a herança melhora a legibilidade do seu código , o que é muito importante ao trabalhar em um projeto grande com várias pessoas.

Aqui está como eu penso sobre isso: quando estou lendo o código de outra pessoa, se vejo que eles estenderam uma classe, vou perguntar que comportamento na superclasse eles estão mudando?

E então eu examinarei o código deles, procurando uma função substituída. Se não vejo, classifico isso como um uso indevido de herança. Eles deveriam ter usado a composição, apenas para me salvar (e todas as outras pessoas da equipe) de 5 minutos de leitura do código!

Aqui está um exemplo concreto: em Java, a JFrameclasse representa uma janela. Para criar uma janela, você cria uma instância JFramee, em seguida, chama funções nessa instância para adicionar itens a ela e mostrá-la.

Muitos tutoriais (mesmo os oficiais) recomendam que você estenda JFramepara poder tratar instâncias de sua classe como instâncias de JFrame. Mas imho (e a opinião de muitos outros) é que esse é um péssimo hábito! Em vez disso, você deve apenas criar uma instância de JFramee chamar funções nessa instância. Não há absolutamente nenhuma necessidade de extensão JFrameneste caso, pois você não está alterando nenhum dos comportamentos padrão da classe pai.

Por outro lado, uma maneira de executar pintura personalizada é estender a JPanelclasse e substituir a paintComponent()função. Este é um bom uso de herança. Se eu estiver examinando seu código e perceber que você estendeu JPanele substituiu a paintComponent()função, saberei exatamente qual comportamento você está mudando.

Se você escrever apenas um código sozinho, pode ser tão fácil usar a herança quanto usar a composição. Mas finja que você está em um ambiente de grupo e que outras pessoas estarão lendo seu código. Favorecer a composição sobre a herança facilita a leitura do código.


Adicionar uma classe de fábrica inteira com um único método que retorna uma instância de um objeto para que você possa usar composição em vez de herança não torna seu código mais legível ... (Sim, você precisa disso se precisar de uma nova instância a cada vez é chamado um método específico.) Isso não significa que a herança seja boa, mas a composição nem sempre melhora a legibilidade.
Jpmc26

1
@ jpmc26 ... por que diabos você precisaria de uma classe factory apenas para criar uma nova instância de uma classe? E como a herança melhora a legibilidade do que você está descrevendo?
Kevin Workman

Não expliquei explicitamente o motivo dessa fábrica no comentário acima? É mais legível porque resulta em menos código para leitura . Você pode realizar o mesmo comportamento com um único método abstrato na classe base e em algumas subclasses. (Você teria uma implementação diferente da sua classe composta para cada uma.) Se os detalhes da construção do objeto estiverem fortemente vinculados à classe base, ele não será muito útil em nenhum outro lugar do código, reduzindo ainda mais os benefícios de fazer uma aula separada. Em seguida, mantém partes estreitamente relacionadas do código próximas umas das outras.
Jpmc26

Como eu disse, isso não significa necessariamente que a herança seja boa, mas exemplifica como a composição não melhora a legibilidade em algumas situações.
Jpmc26

1
Sem ver um exemplo específico, é difícil realmente comentar. Mas o que você descreveu parece um caso de excesso de engenharia para mim. Precisa de uma instância de uma classe? A newpalavra-chave fornece uma. De qualquer forma, obrigado pela discussão.
22717 Kevin Workman
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.