Eu sempre gostei da idéia de ter várias heranças suportadas em um idioma. Na maioria das vezes, embora seja intencionalmente perdoado, e a suposta "substituição" são interfaces. As interfaces simplesmente não cobrem a mesma herança múltipla, e essa restrição pode ocasionalmente levar a mais códigos padronizados.
A única razão básica que eu já ouvi sobre isso é o problema do diamante com as classes base. Eu simplesmente não posso aceitar isso. Para mim, isso é muito parecido com "Bem, é possível estragar tudo, então é automaticamente uma má ideia". Você pode estragar tudo em uma linguagem de programação, e eu quero dizer qualquer coisa. Eu simplesmente não posso levar isso a sério, pelo menos não sem uma explicação mais completa.
Apenas estar ciente desse problema é 90% da batalha. Além disso, acho que ouvi algo anos atrás sobre uma solução alternativa de uso geral envolvendo um algoritmo de "envelope" ou algo parecido (isso soa um sino, alguém?).
Em relação ao problema do diamante, o único problema potencialmente genuíno em que consigo pensar é se você está tentando usar uma biblioteca de terceiros e não consegue ver que duas classes aparentemente não relacionadas nessa biblioteca têm uma classe base comum, mas além de documentação, um recurso de linguagem simples pode, digamos, exigir que você declare especificamente sua intenção de criar um diamante antes que ele realmente compile um para você. Com esse recurso, qualquer criação de diamante é intencional, imprudente ou porque alguém desconhece essa armadilha.
Para que tudo isso seja dito ... Existe alguma razão real para a maioria das pessoas odiar herança múltipla, ou é apenas um monte de histeria que causa mais mal do que bem? Existe algo que eu não estou vendo aqui? Obrigado.
Exemplo
O carro estende o WheeledVehicle, o KIASpectra estende o Car and Electronic, o KIASpectra contém o rádio. Por que o KIASpectra não contém eletrônicos?
Porque é um eletrônico. Herança versus composição deve sempre ser um relacionamento é-um vs. um relacionamento tem-um.
Porque é um eletrônico. Existem fios, placas de circuito, interruptores etc. tudo isso para cima e para baixo.
Porque é um eletrônico. Se sua bateria descarregar no inverno, você terá tantos problemas como se todas as suas rodas de repente desaparecessem.
Por que não usar interfaces? Veja o número 3, por exemplo. Não quero escrever isso repetidamente, e realmente não quero criar uma classe auxiliar de proxy bizarra para fazer isso:
private void runOrDont()
{
if (this.battery)
{
if (this.battery.working && this.switchedOn)
{
this.run();
return;
}
}
this.dontRun();
}
(Não estamos discutindo se essa implementação é boa ou ruim.) Você pode imaginar como pode haver várias dessas funções associadas ao Electronic que não estão relacionadas a nada no WheeledVehicle e vice-versa.
Eu não tinha certeza se deveria me ater a esse exemplo ou não, já que há espaço para interpretação lá. Você também pode pensar em termos de Veículo com extensão de avião e FlyingObject e Pássaro com extensão de Animal e FlyingObject, ou em termos de um exemplo muito mais puro.
Traits
- eles agem como interfaces com implementação opcional, mas têm algumas restrições que ajudam a evitar problemas como o problema do diamante.KiaSpectra
não é umElectronic
; ele tem Electronics, e pode ser umElectronicCar
(que se estenderiaCar
...)Respostas:
Em muitos casos, as pessoas usam herança para fornecer uma característica a uma classe. Por exemplo, pense em um Pegasus. Com herança múltipla, você pode ficar tentado a dizer que o Pegasus estende Cavalo e Pássaro, porque você classificou o Pássaro como um animal com asas.
No entanto, os pássaros têm outras características que Pegasi não. Por exemplo, os pássaros botam ovos, os Pegasi têm nascimento vivo. Se a herança é seu único meio de transmitir características de compartilhamento, não há como excluir a característica de postura de ovos do Pegasus.
Alguns idiomas optaram por tornar os traços uma construção explícita dentro do idioma. Outros o guiam gentilmente nessa direção, removendo o MI do idioma. De qualquer maneira, não consigo pensar em um único caso em que pensei "Cara, eu realmente preciso do MI para fazer isso corretamente".
Também vamos discutir o que realmente é a herança. Ao herdar de uma classe, você depende dessa classe, mas também precisa oferecer suporte aos contratos que a classe suporta, implícitos e explícitos.
Veja o exemplo clássico de um quadrado herdado de um retângulo. O retângulo expõe uma propriedade length e width e também um método getPerimeter e getArea. O quadrado substitui o comprimento e a largura, de modo que, quando um é definido, o outro é definido para corresponder a getPerimeter e getArea funcionaria da mesma maneira (2 * comprimento + 2 * largura para perímetro e comprimento * largura para área).
Há um único caso de teste que é interrompido se você substituir esta implementação de um quadrado por um retângulo.
Já é difícil o suficiente para acertar as coisas com uma única cadeia de herança. Fica ainda pior quando você adiciona outra à mistura.
As armadilhas que mencionei com o Pegasus no MI e as relações Retângulo / Quadrado são os resultados de um design inexperiente para as classes. Basicamente, evitar a herança múltipla é uma maneira de ajudar os desenvolvedores iniciantes a evitar um tiro no próprio pé. Como todos os princípios de design, ter disciplina e treinamento com base neles permite que você descubra, com o tempo, quando é bom sair deles. Veja o Modelo de Aquisição de Habilidades da Dreyfus , no nível Especialista, seu conhecimento intrínseco transcende a confiança em máximas / princípios. Você pode "sentir" quando uma regra não se aplica.
E eu concordo que, de alguma forma, trapacei com um exemplo do "mundo real" de por que o MI é desaprovado.
Vamos olhar para uma estrutura de interface do usuário. Especificamente, vamos ver alguns widgets que, a princípio, podem parecer que são simplesmente uma combinação de dois outros. Como uma caixa de combinação. Um ComboBox é um TextBox que possui um DropDownList de suporte. Ou seja, eu posso digitar um valor ou selecionar em uma lista pré-ordenada de valores. Uma abordagem ingênua seria herdar o ComboBox do TextBox e DropDownList.
Mas sua caixa de texto deriva seu valor do que o usuário digitou. Enquanto o DDL obtém seu valor do que o usuário seleciona. Quem toma precedente? O DDL pode ter sido projetado para verificar e rejeitar qualquer entrada que não estivesse em sua lista original de valores. Nós substituímos essa lógica? Isso significa que precisamos expor a lógica interna para que os herdeiros substituam. Ou pior, adicione lógica à classe base que existe apenas para oferecer suporte a uma subclasse (violando o Princípio de Inversão da Dependência ).
Evitar o MI ajuda a evitar completamente essa armadilha. E pode levar à extração de traços comuns e reutilizáveis dos widgets da interface do usuário, para que possam ser aplicados conforme necessário. Um excelente exemplo disso é a Propriedade anexada do WPF, que permite que um elemento da estrutura no WPF forneça uma propriedade que outro elemento da estrutura possa usar sem herdar do elemento da estrutura pai.
Por exemplo, uma grade é um painel de layout no WPF e possui propriedades anexadas de coluna e linha que especificam onde um elemento filho deve ser colocado na organização da grade. Sem propriedades anexadas, se eu quiser organizar um Botão dentro de uma Grade, o Botão terá que derivar da Grade para que ele possa ter acesso às propriedades de Coluna e Linha.
Os desenvolvedores levaram esse conceito adiante e usaram propriedades anexadas como uma maneira de componente de comportamento (por exemplo, aqui está minha postagem sobre como criar um GridView classificável usando propriedades anexadas escritas antes de o WPF incluir um DataGrid). A abordagem foi reconhecida como um padrão de design XAML chamado Attached Behaviors .
Espero que isso tenha fornecido um pouco mais de informações sobre por que a herança múltipla geralmente é desaprovada.
fonte
Permitir herança múltipla torna as regras sobre sobrecargas de funções e despacho virtual decididamente mais complicadas, bem como a implementação da linguagem em torno dos layouts de objetos. Eles impactam bastante os projetistas / implementadores de linguagem e elevam o nível já alto para obter uma linguagem pronta, estável e adotada.
Outro argumento comum que eu já vi (e que fiz às vezes) é que, ao ter duas classes base, seu objeto quase invariavelmente viola o Princípio de Responsabilidade Única. As duas classes base são boas classes independentes com sua própria responsabilidade (causando a violação) ou são tipos parciais / abstratos que trabalham entre si para criar uma única responsabilidade coesa.
Nesse outro caso, você tem três cenários:
Pessoalmente, acho que a herança múltipla tem uma má reputação, e que um sistema bem feito de composição de estilo de traços seria realmente poderoso / útil ... mas há muitas maneiras pelas quais ela pode ser mal implementada, e por muitas razões não é uma boa ideia em uma linguagem como C ++.
[editar] em relação ao seu exemplo, isso é um absurdo. Um Kia tem eletrônicos. Ele tem um motor. Da mesma forma, seus componentes eletrônicos têm uma fonte de energia, que por acaso é uma bateria de carro. Herança, muito menos herança múltipla, não tem lugar lá.
fonte
A única razão pela qual não é permitido é porque facilita as pessoas a darem um tiro no próprio pé.
O que geralmente se segue neste tipo de discussão são argumentos sobre se a flexibilidade de ter as ferramentas é mais importante do que a segurança de não disparar. Não existe uma resposta decididamente correta para esse argumento, porque, como a maioria das outras coisas na programação, a resposta depende do contexto.
Se seus desenvolvedores estão confortáveis com o MI, e o MI faz sentido no contexto do que você está fazendo, você sentirá muita falta dele em um idioma que não o suporta. Ao mesmo tempo, se a equipe não se sentir confortável com isso ou se não houver necessidade real e as pessoas o usarem 'apenas porque podem', isso é contraproducente.
Mas não, não existe um argumento absolutamente verdadeiro e convincente que prove que a herança múltipla seja uma má ideia.
EDITAR
As respostas a esta pergunta parecem ser unânimes. Por uma questão de ser o advogado do diabo, darei um bom exemplo de herança múltipla, onde não fazê-lo leva a hacks.
Suponha que você esteja projetando um aplicativo do mercado de capitais. Você precisa de um modelo de dados para títulos. Alguns títulos são produtos de ações (ações, fundos de investimento imobiliário, etc.) outros são dívida (títulos, títulos corporativos), outros são derivativos (opções, futuros). Portanto, se você estiver evitando o MI, criará uma árvore de herança simples e muito clara. Uma ação herdará patrimônio, o título herdará dívida. Ótimo até agora, mas e os derivativos? Eles podem ser baseados em produtos do tipo Equity ou no tipo Débito? Ok, acho que faremos com que nossa árvore de herança se ramifique mais. Lembre-se de que alguns derivativos são baseados em produtos patrimoniais, produtos de dívida ou nenhum. Portanto, nossa árvore de herança está ficando complicada. Então, vem o analista de negócios e informa que agora apoiamos títulos indexados (opções de indexação, opções futuras de indexação). E essas coisas podem ser baseadas em ações, dívidas ou derivativos. Isso está ficando bagunçado! Minha opção futura de índice deriva de Equidade-> Estoque-> Opção-> Índice? Por que não Equidade-> Ações-> Índice-> Opção? E se um dia eu encontrar os dois no meu código (Isso aconteceu; história verdadeira)?
O problema aqui é que esses tipos fundamentais podem ser misturados em qualquer permutação que não derive naturalmente um do outro. Como os objetos são definidos por um é um relacionamento, a composição não faz nenhum sentido. Herança múltipla (ou o conceito semelhante de mixins) é a única representação lógica aqui.
A solução real para esse problema é ter os tipos de ações, dívidas, derivativos e índices definidos e misturados usando herança múltipla para criar seu modelo de dados. Isso criará objetos que ambos fazem sentido e emprestam facilmente para reutilização de código.
fonte
Equity
eDebt
ambos implementamISecurity
.Derivative
tem umaISecurity
propriedade Pode ser umISecurity
caso apropriado (não sei finanças).IndexedSecurities
contenha novamente uma propriedade de interface aplicada aos tipos dos quais é permitido basear-se. Se todos eles sãoISecurity
, em seguida, todos eles têmISecurity
propriedades e pode ser arbitrariamente aninhados ...As outras respostas aqui parecem estar entrando principalmente na teoria. Então, aqui está um exemplo concreto de Python, simplificado, que eu realmente me deparei com isso, o que exigiu uma boa quantidade de refatoração:
Bar
foi escrito assumindo que teve sua própria implementaçãozeta()
, o que geralmente é uma suposição muito boa. Uma subclasse deve substituí-la conforme apropriado para que faça a coisa certa. Infelizmente, os nomes eram coincidentemente iguais - eles faziam coisas bastante diferentes, masBar
agora estavam chamandoFoo
a implementação:É bastante frustrante quando não há erros gerados, o aplicativo começa a agir levemente errado e a alteração do código que o causou (criando
Bar.zeta
) não parece estar onde está o problema.fonte
super()
?super()
tem em torno deBang
paraBar, Foo
também resolveria o problema - mas seu argumento é bom, +1.Bar.zeta(self)
.Eu diria que não há problemas reais com o MI no idioma certo . A chave é permitir estruturas de diamante, mas exige que os subtipos forneçam sua própria substituição, em vez de o compilador escolher uma das implementações com base em alguma regra.
Faço isso na Guava , um idioma em que estou trabalhando. Uma característica do Guava é que podemos invocar a implementação de um método de um supertipo específico. Portanto, é fácil indicar qual implementação de supertipo deve ser "herdada", sem nenhuma sintaxe especial:
Se não dermos o
OrderedSet
seu própriotoString
, obteremos um erro de compilação. Sem surpresas.Acho que o MI é particularmente útil nas coleções. Por exemplo, eu gosto de usar um
RandomlyEnumerableSequence
tipo para evitar a declaraçãogetEnumerator
de matrizes, deques e assim por diante:Se não tivéssemos MI, poderíamos escrever um
RandomAccessEnumerator
para várias coleções, mas ter que escrever um brevegetEnumerator
método ainda adiciona um padrão.Da mesma forma, MI é útil para herdar implementações padrão de
equals
,hashCode
etoString
para coleções.fonte
toString
o comportamento de um usuário, portanto, substituí-lo não viola o princípio da substituição. Às vezes, você também recebe "conflitos" entre métodos que têm o mesmo comportamento, mas com algoritmos diferentes, especialmente com coleções.Ordered extends Set and Sequence
umDiamond
? É apenas uma junção. Falta ohat
vértice. Por que você chama isso de diamante? Eu perguntei aqui, mas parece uma pergunta tabu. Como você soube que precisa chamar essa estrutura triangular de diamante em vez de ingressar?Top
tipo implícito que declaratoString
, então havia realmente uma estrutura de diamante. Mas acho que você tem razão - uma "junção" sem uma estrutura de diamante cria um problema semelhante, e a maioria dos idiomas lida com os dois casos da mesma maneira.A herança, múltipla ou não, não é tão importante. Se dois objetos de tipo diferente são substituíveis, é isso que importa, mesmo que não estejam vinculados por herança.
Uma lista vinculada e uma cadeia de caracteres têm pouco em comum e não precisam ser vinculadas por herança, mas é útil se eu puder usar uma
length
função para obter o número de elementos em qualquer um deles.A herança é um truque para evitar a implementação repetida do código. Se a herança economiza seu trabalho, e a herança múltipla economiza ainda mais trabalho em comparação à herança única, essa é toda a justificativa necessária.
Suspeito que algumas línguas não implementem herança múltipla muito bem, e para os profissionais dessas línguas, é isso que significa herança múltipla. Mencione herança múltipla a um programador de C ++, e o que vem à mente é algo sobre problemas quando uma classe termina com duas cópias de uma base por dois caminhos de herança diferentes, se deve ser usada
virtual
em uma classe base e confusão sobre como os destruidores são chamados , e assim por diante.Em muitas línguas, a herança de classe é confundida com a herança de símbolos. Quando você deriva uma classe D de uma classe B, não apenas está criando um relacionamento de tipo, mas como essas classes também servem como espaços de nomes lexicais, você está lidando com a importação de símbolos do espaço de nomes B para o espaço de nomes D, além de a semântica do que está acontecendo com os tipos B e D. A herança múltipla traz, portanto, questões de conflito de símbolos. Se herdarmos de
card_deck
egraphic
, os quais "têm" umdraw
método, o que isso significa paradraw
o objeto resultante? Um sistema de objetos que não apresenta esse problema é o do Common Lisp. Talvez não por coincidência, a herança múltipla seja usada nos programas Lisp.Mal implementado, qualquer coisa inconveniente (como herança múltipla) deve ser odiada.
fonte
length
operação é útil independentemente do conceito de herança (que, neste caso, não ajuda: é improvável que consigamos alcançar algo tentando compartilhar a implementação entre os dois métodos dalength
função). No entanto, pode haver alguma herança abstrata: por exemplo, list e string são do tipo sequência (mas que não fornecem nenhuma implementação).Até onde eu sei, parte do problema (além de tornar seu design um pouco mais difícil de entender (ainda mais fácil de codificar)) é que o compilador economizará espaço suficiente para os dados de sua classe, permitindo uma quantidade enorme de desperdício de memória no seguinte caso:
(Meu exemplo pode não ser o melhor, mas tente entender o espaço múltiplo de memória para o mesmo objetivo, foi a primeira coisa que me veio à cabeça: P)
Considere um DDD em que o cão da classe se estende de caninus e animal de estimação, um caninus tem uma variável que indica a quantidade de alimento que deve comer (um número inteiro) sob o nome dietKg, mas um animal de estimação também tem outra variável para esse fim, geralmente sob o mesmo name (a menos que você defina outro nome de variável, você terá que codificar um código extra, que era o problema inicial que você queria evitar, para manipular e manter a integridade das variáveis bucais); então, você terá dois espaços de memória para a exata mesmo objetivo, para evitar isso, você precisará modificar seu compilador para reconhecer esse nome no mesmo espaço para nome e apenas atribuir um único espaço de memória a esses dados, o que infelizmente é impossível determinar em tempo de compilação.
Você pode, é claro, projetar um idioma para especificar que essa variável já deve ter um espaço definido em outro lugar, mas no final o programador deve especificar onde está o espaço de memória ao qual essa variável está se referindo (e novamente código extra).
Confie em mim, as pessoas que estão implementando esse pensamento muito sobre tudo isso, mas fico feliz que você tenha perguntado, sua espécie de perspectiva é a que muda de paradigma;) e, se você considerar isso, não estou dizendo que é impossível (mas muitas suposições e um compilador multifásico deve ser implementado e realmente complexo); estou apenas dizendo que ele ainda não existe. Se você iniciar um projeto para seu próprio compilador capaz de fazer "isso" (herança múltipla), informe-me. Será um prazer participar da sua equipe.
fonte
Por um bom tempo agora, nunca me ocorreu realmente como algumas tarefas de programação são totalmente diferentes de outras - e quanto ajuda se as linguagens e os padrões usados forem adaptados ao espaço do problema.
Quando você está trabalhando sozinho ou principalmente isolado no código que você escreveu, é um espaço de problema completamente diferente de herdar uma base de código de 40 pessoas na Índia que trabalharam nele por um ano antes de entregá-lo a você sem nenhuma ajuda de transição.
Imagine que você acabou de ser contratado pela empresa dos seus sonhos e depois herdou essa base de código. Além disso, imagine que os consultores estavam aprendendo sobre (e, portanto, apaixonados por) herança e herança múltipla ... Você poderia imaginar o que poderia estar trabalhando?
Quando você herda o código, o recurso mais importante é que ele é compreensível e as partes são isoladas para que possam ser trabalhadas independentemente. É claro que quando você escreve pela primeira vez as estruturas de código, como herança múltipla, pode economizar um pouco de duplicação e parecer se encaixar no seu humor lógico na época, mas o próximo cara tem mais coisas para desembaraçar.
Toda interconexão no seu código também dificulta a compreensão e a modificação de partes de forma independente, duplamente com herança múltipla.
Quando você trabalha como parte de uma equipe, deseja segmentar o código mais simples possível, que não oferece absolutamente nenhuma lógica redundante (é isso que DRY realmente significa, não que você não deva digitar muito, apenas para nunca precisar alterar seu código em 2). locais para resolver um problema!)
Existem maneiras mais simples de obter o código DRY do que a herança múltipla; portanto, incluí-lo em um idioma pode apenas abrir problemas inseridos por outras pessoas que podem não estar no mesmo nível de entendimento que você. Só é tentador se o seu idioma não puder oferecer uma maneira simples / menos complexa de manter seu código SECO.
fonte
O maior argumento contra a herança múltipla é que algumas habilidades úteis podem ser fornecidas e alguns axiomas úteis podem ser mantidos, em uma estrutura que a restringe severamente (*), mas que não pode ser fornecida e / ou mantida sem essas restrições. Entre eles:
A capacidade de ter vários módulos compilados separadamente inclui classes que herdam das classes de outros módulos e recompila um módulo contendo uma classe base sem precisar recompilar todos os módulos que herdam dessa classe base.
A capacidade de um tipo herdar uma implementação de membro de uma classe pai sem que o tipo derivado precise implementá-lo novamente
O axioma de que qualquer instância de objeto pode ser upcast ou downcast diretamente para si ou para qualquer um de seus tipos base, e tais upcasts e downcasts, em qualquer combinação ou sequência, sempre preservam a identidade
O axioma de que se uma classe derivada substituir e encadear um membro da classe base, o membro base será chamado diretamente pelo código de encadeamento e não será chamado em nenhum outro lugar.
(*) Normalmente, exigindo que os tipos que suportam herança múltipla sejam declarados como "interfaces" em vez de classes, e não permitindo que interfaces façam tudo o que as classes normais podem fazer.
Se alguém deseja permitir herança múltipla generalizada, outra coisa deve ceder. Se X e Y herdam de B, ambos substituem o mesmo membro M e encadeiam a implementação base e se D herda de X e Y, mas não substitui M, dada uma instância q do tipo D, o que deve (( B) q) .M () faz? A exclusão de tal conversão violaria o axioma que diz que qualquer objeto pode ser upcast para qualquer tipo de base, mas qualquer comportamento possível da chamada de conversão e membro violaria o axioma em relação ao encadeamento de métodos. Pode-se exigir que as classes sejam carregadas apenas em combinação com a versão específica de uma classe base na qual elas foram compiladas, mas isso geralmente é estranho. Se o tempo de execução se recusar a carregar qualquer tipo no qual qualquer ancestral possa ser alcançado por mais de uma rota, pode ser viável, mas limitaria bastante a utilidade da herança múltipla. Permitir caminhos de herança compartilhados somente quando não houver conflitos criaria situações em que uma versão antiga do X seria compatível com um Y antigo ou novo e um Y antigo seria compatível com um X antigo ou novo, mas o novo X e o novo Y seriam ser compatível, mesmo que nada do que, por si só, deva ser esperado, seja uma mudança radical.
Algumas linguagens e estruturas permitem herança múltipla, na teoria de que o que é ganho com o MI é mais importante do que o que deve ser dado para permitir. Os custos do MI são significativos, no entanto, e para muitos casos, as interfaces fornecem 90% dos benefícios do MI por uma pequena fração do custo.
fonte
FirstDerivedFoo
a substituição deBar
nada faz, mas acorrenta a um não virtual protegidoFirstDerivedFoo_Bar
, eSecondDerivedFoo
a substituição de cadeias a um não virtual protegidoSecondDerivedBar
, e se o código que deseja acessar o método base usa esses métodos não virtuais protegidos, pode ser uma abordagem viável ...base.VirtualMethod
), mas não conheço nenhum suporte de idioma para facilitar particularmente essa abordagem. Eu gostaria que houvesse uma maneira limpa de anexar um bloco de código a um método protegido não virtual e a um método virtual com a mesma assinatura sem exigir um método virtual "de uma linha" (que acaba levando muito mais que uma linha) no código fonte).