Existe alguma razão “real” para a herança múltipla ser odiada?

123

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?

  1. Porque é um eletrônico. Herança versus composição deve sempre ser um relacionamento é-um vs. um relacionamento tem-um.

  2. Porque é um eletrônico. Existem fios, placas de circuito, interruptores etc. tudo isso para cima e para baixo.

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

Panzercrisis
fonte
24
também incentiva a herança sobre a composição ... (quando deveria ser o contrário)
aberração catraca
34
"Você pode estragar tudo" é um motivo perfeitamente válido para remover um recurso. O design moderno da linguagem está um pouco fragmentado nesse ponto, mas geralmente você pode classificar os idiomas em "Potência da restrição" e "Potência da flexibilidade". Nenhum dos quais está "correto", eu acho, ambos têm pontos fortes a fazer. O MI é uma daquelas coisas que costumam ser usadas para o mal com mais freqüência e, portanto, idiomas restritivos o removem. Os flexíveis não, porque "mais frequentemente do que não" não é "literalmente sempre". Dito isto, mixins / características são uma solução melhor para o caso geral, eu acho.
Phoshi
8
Existem alternativas mais seguras para herança múltipla em vários idiomas, que vão além das interfaces. Confira os Scala 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.
KChaloux
5
Veja também esta pergunta anteriormente encerrada: programmers.stackexchange.com/questions/122480/…
Doc Brown
19
KiaSpectranão é um Electronic ; ele tem Electronics, e pode ser um ElectronicCar(que se estenderia Car...)
Brian S

Respostas:

68

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.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

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.

Michael Brown
fonte
14
"Cara, eu realmente preciso do MI para fazer isso corretamente" - veja minha resposta para um exemplo. Em suma, conceitos ortogonais que satisfazem um relacionamento é-um fazem sentido para MI. Eles são muito raros, mas existem.
MrFox
13
Cara, eu odeio esse exemplo de retângulo quadrado. Existem muitos outros exemplos esclarecedores que não exigem mutabilidade.
asmeurer
9
Eu amo que a resposta para "fornecer um exemplo real" fosse discutir uma criatura fictícia. Excelente ironia +1
jjathman
3
Então você está dizendo que a herança múltipla é ruim, porque a herança é ruim (seus exemplos são sobre herança de interface única). Portanto, com base apenas nesta resposta, uma linguagem deve se livrar da herança e das interfaces ou ter herança múltipla.
Ctrl-alt-delor
12
Eu não acho que esses exemplos realmente funcionem ... ninguém diz que um pégaso é um pássaro e um cavalo ... você diz que é um WingedAnimal e um HoofedAnimal. O exemplo de quadrado e retângulo faz um pouco mais de sentido porque você se encontra com um objeto que se comporta de maneira diferente do que você pensaria, com base em sua definição declarada. No entanto, acho que essa situação seria culpa dos programadores por não entender isso (se isso realmente provou ser um problema).
Richard
60

Existe algo que eu não estou vendo aqui?

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:

  1. A não sabe nada sobre B - Ótimo, você pode combinar as aulas porque teve sorte.
  2. A sabe sobre B - Por que A não herdou apenas B?
  3. A e B se conhecem - Por que você não fez apenas uma aula? Qual o benefício de tornar essas coisas tão acopladas, mas parcialmente substituíveis?

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

Telastyn
fonte
8
Outra questão interessante é como as classes base + suas classes base são inicializadas. Encapsulamento> Herança.
jozefg
"um sistema bem feito de composição de estilo de traço seria realmente poderoso / útil ..." - Estes são conhecidos como mixins e são poderosos / úteis. Os mixins podem ser implementados usando o MI, mas a Herança Múltipla não é necessária para os mixins. Alguns idiomas suportam mixins inerentemente, sem MI.
BlueRaja - Danny Pflughoeft
Para Java em particular, já temos várias interfaces e o INVOKEINTERFACE, para que o MI não tenha implicações óbvias no desempenho.
Daniel Lubarov
Tetastyn, concordo que o exemplo foi ruim e não serviu à sua pergunta. A herança não é para adjetivos (eletrônicos), é para substantivos (veículo).
Richard
29

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.

MrFox
fonte
27
Eu trabalhei em finanças. Há uma razão pela qual a programação funcional é preferível à OO em software financeiro. E você apontou isso. O MI não corrige esse problema, ele o agrava. Acredite em mim, não posso lhe dizer quantas vezes ouvi "É assim ... exceto" quando lida com clientes financeiros. Isso se torna a desgraça da minha existência.
Michael Brown
8
Isso parece muito solucionável com interfaces para mim ... na verdade, parece mais adequado para interfaces do que qualquer outra coisa. Equitye Debtambos implementam ISecurity. Derivativetem uma ISecuritypropriedade Pode ser um ISecuritycaso apropriado (não sei finanças). IndexedSecuritiescontenha novamente uma propriedade de interface aplicada aos tipos dos quais é permitido basear-se. Se todos eles são ISecurity, em seguida, todos eles têm ISecuritypropriedades e pode ser arbitrariamente aninhados ...
Bobson
11
@Bobson, o problema é que você precisa reimplementar a interface para cada implementador e, em muitos casos, é a mesma coisa. Você pode usar delegação / composição, mas depois perde o acesso a membros privados. E como o Java não possui delegados / lambdas ... não há literalmente uma boa maneira de fazê-lo. Exceto, possivelmente com uma ferramenta Aspect como Aspect4J
Michael Brown
10
@ Bobson exatamente o que Mike Brown disse. Sim, você pode projetar uma solução com interfaces, mas será desajeitada. Sua intuição de usar interfaces é muito correta, porém, é um desejo oculto de mixins / herança múltipla :).
MrFox
11
Isso toca uma singularidade em que os programadores de OO preferem pensar em termos de herança e muitas vezes deixam de aceitar a delegação como uma abordagem de OO válida, que normalmente produz uma solução mais enxuta, mais sustentável e extensível. Tendo trabalhado em finanças e assistência médica, vi a bagunça incontrolável que o MI pode criar, especialmente porque as leis tributárias e de saúde mudam ano após ano e a definição de um objeto ÚLTIMO ano é inválida para ESTE ano (ainda deve desempenhar a função de qualquer um dos anos e deve ser intercambiável). A composição produz um código mais enxuto, mais fácil de testar e custa menos com o tempo.
Shaun Wilson
11

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:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barfoi escrito assumindo que teve sua própria implementação zeta(), 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, mas Baragora estavam chamando Fooa implementação:

foozeta
---
barstuff
foozeta

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

Izkata
fonte
Como é evitado o problema do diamante através de chamadas para super()?
Panzercrisis
@ Panzercrisis Desculpe, esqueça essa parte. I misremembered que o problema do "problema diamante" geralmente se refere a - Python teve uma questão separada causada por herança em forma de diamante que super()tem em torno de
Izkata
5
Fico feliz que o MI do C ++ seja muito melhor projetado. O bug acima simplesmente não é possível. Você deve desambiguar manualmente antes que o código seja compilado.
Thomas Eding
2
Alterar a ordem da herança Bangpara Bar, Footambém resolveria o problema - mas seu argumento é bom, +1.
Sean Vieira
11
@ThomasEding Você pode disambiguate manualmente ainda: Bar.zeta(self).
Tyler Crompton
9

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:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Se não dermos o OrderedSetseu próprio toString, obteremos um erro de compilação. Sem surpresas.

Acho que o MI é particularmente útil nas coleções. Por exemplo, eu gosto de usar um RandomlyEnumerableSequencetipo para evitar a declaração getEnumeratorde matrizes, deques e assim por diante:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Se não tivéssemos MI, poderíamos escrever um RandomAccessEnumeratorpara 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, hashCodee toStringpara coleções.

Daniel Lubarov
fonte
11
@AndyzSmith, entendo o seu ponto, mas nem todos os conflitos de herança são problemas semânticos reais. Considere o meu exemplo - nenhum dos tipos faz promessas sobre toStringo 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.
Daniel Lubarov
11
@ Asmageddon concordou, meus exemplos envolvem apenas funcionalidade. O que você vê como as vantagens dos mixins? Pelo menos em Scala, os traços são essencialmente apenas classes abstratas que permitem MI - eles ainda podem ser usados ​​para identidade e ainda trazer o problema do diamante. Existem outros idiomas onde os mixins têm diferenças mais interessantes?
Daniel Lubarov
11
Classes tecnicamente abstratas podem ser usadas como interfaces, para mim, o benefício simplesmente do fato de a construção ser uma "dica de uso", o que significa que eu sei o que esperar quando vejo o código. Dito isto, atualmente estou codificando em Lua, que não possui modelo OOP inerente - os sistemas de classes existentes geralmente permitem incluir tabelas como mixins, e o que estou codificando usa classes como mixins. A única diferença é que você só pode usar herança (única) para identidade, mixins para funcionalidade (sem informações de identidade) e interfaces para verificações adicionais. Talvez não seja de alguma forma muito melhor, mas faz sentido para mim.
Llamageddon
2
Por que você chamar a Ordered extends Set and Sequenceum Diamond? É apenas uma junção. Falta o hatvé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?
Val
11
@RecognizeEvilasWaste esse idioma tem um Toptipo implícito que declara toString, 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.
Daniel Lubarov
7

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 lengthfunçã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 virtualem 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_decke graphic, os quais "têm" um drawmétodo, o que isso significa para drawo 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.

Kaz
fonte
2
Seu exemplo com o método length () de contêineres de lista e string é ruim, pois esses dois estão fazendo coisas completamente diferentes. O objetivo da herança não é reduzir a repetição de código. Se você deseja reduzir a repetição de código, refatorar partes comuns em uma nova classe.
BЈовић
11
@ BЈовић Os dois não estão fazendo coisas "completamente" diferentes.
Kaz
11
Compartindo string e lista , pode-se ver muita repetição. Usar a herança para implementar esses métodos comuns seria simplesmente errado.
BЈовић
11
@ BЈовић Não é dito na resposta que uma string e uma lista devem ser vinculadas por herança; muito pelo contrário. O comportamento de poder substituir uma lista ou string em uma lengthoperaçã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 da lengthfunçã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).
Kaz
2
Além disso, a herança é uma maneira de refatorar partes para uma classe comum: a classe base.
Kaz
1

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.

Ordiel
fonte
11
Em que momento um compilador poderia assumir que variáveis ​​com o mesmo nome de diferentes classes pai são seguras para combinar? Que garantia você tem de que caninus.diet e pet.diet realmente servem ao mesmo objetivo? O que você faria com a função / métodos com o mesmo nome?
Mr.Mindor
hahaha Estou totalmente de acordo com você @ Mr.Mindor, é exatamente o que estou dizendo na minha resposta, a única maneira que me ocorre de me aproximar dessa abordagem é a programação orientada a protótipo, mas não estou dizendo que é impossível , e essa é exatamente a razão pela qual eu disse que muitas suposições devem ser feitas durante o tempo de compilação e alguns "dados" / especificações extras devem ser escritos pelo programador (no entanto, é mais codificação, que era o problema original)
Ordiel
-1

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.

Bill K
fonte
Além disso, imagine que os consultores estavam aprendendo - eu já vi tantas linguagens não MI (por exemplo, net) em que os consultores enlouqueceram com DI e IoC baseados em interface para tornar a coisa toda uma bagunça impenetrável. MI não piora isso, provavelmente melhora.
Gbjbaanb
@gbjbaanb Você está certo, é fácil para alguém que não foi queimado atrapalhar qualquer recurso - na verdade, é quase um requisito para aprender um recurso que você estrague tudo para ver como usá-lo da melhor maneira. Estou meio curioso porque você parece estar dizendo que não ter MI força mais DI / IoC que eu não vi - eu não pensaria que o código de consultor que você obteve com todas as interfaces ruins / DI teria sido melhor também ter uma árvore de herança muitos-para-muitos louca, mas poderia ser minha inexperiência com o MI. Você tem uma página que eu poderia ler sobre a substituição de DI / IoC por MI?
Bill K
-3

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.

supercat
fonte
Os eleitores de baixo pretendem comentar? Acredito que minha resposta forneça informações não presentes em outros idiomas, no que diz respeito às vantagens semânticas que podem ser recebidas ao proibir a herança múltipla. O fato de permitir a herança múltipla exigir renunciar a outras coisas úteis seria uma boa razão para quem valoriza essas outras coisas mais do que a herança múltipla para se opor ao MI.
Supercat
2
Os problemas com o MI são facilmente resolvidos ou evitados usando as mesmas técnicas que você usaria com herança única. Nenhuma das habilidades / axiomas que você mencionou é inerentemente prejudicada pelo MI, exceto pelo segundo ... e se você está herdando de duas classes que fornecem comportamentos diferentes para o mesmo método, cabe a você resolver essa ambiguidade de qualquer maneira. Mas se você tiver que fazer isso, provavelmente já estará abusando da herança.
cHao
@cHao: Se alguém exige que as classes derivadas substituam os métodos-pai e não os encadeie (mesma regra que as interfaces), pode-se evitar os problemas, mas essa é uma limitação bastante grave. Se alguém usa um padrão no qual FirstDerivedFooa substituição de Barnada faz, mas acorrenta a um não virtual protegido FirstDerivedFoo_Bar, e SecondDerivedFooa substituição de cadeias a um não virtual protegido SecondDerivedBar, 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 ...
supercat
... (evitando a sintaxe 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).
Supercat
Não existe um requisito inerente ao MI que as classes não chamam métodos base, e não há uma razão específica para que um seja necessário. Parece um pouco estranho culpar o MI, considerando que o problema também afeta o SI.
cHao