Existe algum motivo “real” para a herança múltipla ser odiada?

120

Sempre gostei da ideia de ter várias heranças suportadas em um idioma. Na maioria das vezes, é intencionalmente esquecido, e a suposta "substituição" é de interfaces. Interfaces simplesmente não cobrem a mesma herança múltipla terrestre, e essa restrição pode ocasionalmente levar a mais código clichê.

A única razão básica que eu já ouvi para isso é o problema dos diamantes com as classes base. Eu simplesmente não posso aceitar isso. Para mim, é muito provável que "Bem, é possível estragar tudo, então é automaticamente uma má idéia". 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, eu acho que ouvi algo anos atrás sobre um trabalho geral envolvendo um algoritmo de "envelope" ou algo assim (isso soa um pouco, alguém?).

Em relação ao problema dos diamantes, 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 da documentação, um recurso de linguagem simples pode, digamos, exigir que você declare especificamente sua intenção de criar um diamante antes de realmente compilar um para você. Com esse recurso, qualquer criação de um diamante é intencional, imprudente ou porque não se sabe dessa armadilha.

Então, tudo o que foi dito ... Existe alguma razão real que a maioria das pessoas odeie herança múltipla, ou é tudo apenas um monte de histeria que causa mais mal do que bem? Existe algo que não estou vendo aqui? Obrigado.

Exemplo

O carro estende o WheeledVehicle, o KIASpectra amplia o Car and Electronic, o KIASpectra contém o rádio. Por que o KIASpectra não contém eletrônica?

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

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

  3. Porque é um eletrônico. Se a sua bateria ficar inoperante no inverno, você terá tantos problemas quanto se todas as rodas de repente desaparecessem.

Por que não usar interfaces? Tome # 3, por exemplo. Eu não quero escrever isso de novo e de novo, e eu realmente não quero criar alguma 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 investigando 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 basear nesse exemplo ou não, já que há espaço para interpretação. Você também pode pensar em termos de Plane extendendo Vehicle e FlyingObject e Bird estendendo Animal e FlyingObject, ou em termos de um exemplo muito mais puro.

    
por Panzercrisis 14.11.2013 / 16:59
fonte

9 respostas

68

Em muitos casos, as pessoas usam herança para fornecer uma característica a uma classe. Por exemplo, pense em um Pégaso. Com herança múltipla, você pode ficar tentado a dizer que o Pégaso estende Cavalo e Pássaro, porque você classificou o Pássaro como um animal com asas.

No entanto, os Birds têm outras características que os Pegasi não possuem. Por exemplo, os pássaros põem ovos, os pégasos têm um nascimento vivo. Se a herança é seu único meio de transmitir traços de compartilhamento, então não há como excluir o traço de postura de ovos do Pégaso.

Algumas linguagens optaram por tornar as características uma construção explícita dentro da linguagem. Outros o guiam suavemente nessa direção, removendo o MI do idioma. De qualquer forma, não consigo pensar em um único caso em que pensei "Cara, eu realmente preciso que o MI faça isso corretamente".

Também vamos discutir o que REALMENTE é a herança. Quando você herda de uma classe, você depende dessa classe, mas também tem que suportar os contratos que essa classe suporta, implícita e explícita.

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 substituiria comprimento e largura para que, quando um é definido, o outro seja definido para corresponder a getPerimeter e getArea funcionaria da mesma forma (2 * comprimento + 2 * largura para perímetro e comprimento * largura para área).

Há um único caso de teste que é interrompido se você substituir essa 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

É difícil o suficiente para acertar as coisas com uma única cadeia de herança. Fica ainda pior quando você adiciona outro ao mix.

As armadilhas que mencionei com o Pegasus em MI e as relações Rectangle / Square são os resultados de um design inexperiente para as classes. Basicamente, evitar múltiplas heranças é uma maneira de ajudar os desenvolvedores iniciantes a evitarem atirar no próprio pé. Como todos os princípios de design, a disciplina e o treinamento baseados neles permitem que você descubra quando é aceitável romper com eles. Veja o Modelo Dreyfus de Aquisição de Habilidades , no nível Expert, 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 eu, de certa forma, trapaceiei com um exemplo do "mundo real" de por que o MI é desaprovado.

Vamos dar uma olhada em uma estrutura de interface do usuário. Especificamente, vamos dar uma olhada em alguns widgets que podem, à primeira vista, parecer que são simplesmente uma combinação de dois outros. Como uma caixa de combinação. Um ComboBox é um TextBox que tem um DropDownList de suporte. Ou seja Eu posso digitar um valor ou posso selecionar de uma lista pré-ordenada de valores. Uma abordagem ingênua seria herdar o ComboBox de TextBox e DropDownList.

Mas sua caixa de texto deriva seu valor do que o usuário digitou. Enquanto o DDL recebe seu valor do que o usuário seleciona. Quem leva precedente? A DDL pode ter sido projetada 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 temos que expor a lógica interna para que os herdeiros sejam anulados. Ou pior, adicione lógica à classe base que está lá apenas para suportar uma subclasse (violando o Princípio da Inversão de Dependência ) .

Evitar MI ajuda você a evitar essa armadilha. E pode levar você a extrair traços comuns e reutilizáveis de seus widgets de interface do usuário para que possam ser aplicados conforme necessário. Um excelente exemplo disso é a Propriedade anexada do WPF que permite uma elemento de estrutura no WPF para fornecer uma propriedade que outro elemento de estrutura pode usar sem herdar do elemento de estrutura pai.

Por exemplo, um Grid é um painel de layout no WPF e possui propriedades anexadas Column e Row que especificam onde um elemento filho deve ser colocado na organização da grade. Sem propriedades anexadas, se eu quiser organizar um Button dentro de um Grid, o Button teria que derivar do Grid para que ele pudesse ter acesso às propriedades Column e Row.

Os desenvolvedores levaram esse conceito adiante e usaram as propriedades anexadas como forma de compor o comportamento (por exemplo, aqui está o meu post sobre como fazer um GridView classificável usando propriedades anexadas escritas antes do WPF incluir um DataGrid). A abordagem foi reconhecida como um padrão de design XAML chamado Comportamentos anexados .

Espero que isso forneça um pouco mais de detalhes sobre por que a herança múltipla geralmente é desaprovada.

    
por 14.11.2013 / 17:41
fonte
60

Is there something that I am not seeing here?

Permitir herança múltipla torna as regras sobre sobrecargas de função e envio virtual mais decididamente mais complicadas, assim como a implementação da linguagem em torno dos layouts de objetos. Isso impacta bastante os designers / implementadores de linguagem e eleva o nível já alto para que uma linguagem seja feita, estável e adotada.

Outro argumento comum que vi (e feito às vezes) é que por ter duas classes de base, seu objeto quase invariavelmente viola o Princípio de Responsabilidade Única. As duas classes base + são boas classes autônomas com responsabilidade própria (causando a violação) ou são tipos parciais / abstratos que trabalham entre si para criar uma responsabilidade coesa única.

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 apenas herdou de B?
  3. A e B conhecem um ao outro - 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 herança múltipla tem um mau rap, e que um sistema bem feito de composição de estilo de traço seria realmente poderoso / útil ... mas há muitas maneiras que podem ser implementadas mal e muito de razões, não é uma boa ideia em uma linguagem como o C ++.

[editar] em relação ao seu exemplo, isso é um absurdo. Um Kia tem eletrônicos. tem um mecanismo. Da mesma forma, é eletrônica tem uma fonte de energia, que só acontece de ser uma bateria de carro. Herança, e muito menos herança múltipla não tem lugar aí.

    
por 14.11.2013 / 17:25
fonte
29

A única razão pela qual não é permitido é porque torna mais fácil para as pessoas atirarem no próprio pé.

O que geralmente se segue neste tipo de discussão é discutir se a flexibilidade de ter as ferramentas é mais importante do que a segurança de não atirar em seu pé. Não há uma resposta decididamente correta para esse argumento, porque, como a maioria das outras coisas na programação, a resposta depende do contexto.

Se os seus desenvolvedores estão confortáveis com o MI, e o MI faz sentido no contexto do que você está fazendo, então você irá sentir muita falta em um idioma que não o suporte. Ao mesmo tempo, se a equipe não estiver confortável com ela, ou se não houver necessidade real dela e as pessoas a usarem "apenas porque puderem", isso é contraproducente.

Mas não, não existe um argumento absolutamente convincente e absolutamente convincente que prove que uma herança múltipla é uma má idéia.

EDITAR

As respostas a esta pergunta parecem ser unânimes. Por causa de ser o advogado do diabo eu vou fornecer um bom exemplo de herança múltipla, onde não fazer isso leva a hacks.

Suponha que você esteja projetando um aplicativo de mercado de capitais. Você precisa de um modelo de dados para títulos. Alguns valores mobiliários são produtos de capital (ações, fundos de investimento imobiliário, etc), outros são dívida (títulos, obrigações empresariais), outros são derivados (opções, futuros). Então, se você está evitando MI, você fará uma árvore de herança muito clara e simples. Uma ação herdará o patrimônio, a obrigação herdará a dívida. Ótimo até agora, mas e os derivados? Eles podem ser baseados em produtos semelhantes a ações ou produtos semelhantes a débito? Ok, eu acho que vamos fazer a nossa árvore de herança sair mais. Tenha em mente que alguns derivativos são baseados em produtos de capital, produtos de dívida ou nenhum deles. Então nossa árvore de herança está ficando complicada. Depois vem o analista de negócios e diz que agora suportamos títulos indexados (opções de índice, opções futuras de índice). E essas coisas podem ser baseadas em Equidade, Dívida ou Derivada. Isso está ficando confuso! A minha opção de índice futuro deriva Equity- > Stock- > Option- > Index? Por que não Equity- > Stock- > Index- > Opção? E se um dia eu encontrar ambos 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 deriva naturalmente um do outro. Os objetos são definidos por um relacionamento é , portanto, a composição não faz sentido algum. Herança múltipla (ou o conceito similar de mixins) é a única representação lógica aqui.

A solução real para este problema é ter os tipos Equity, Debt, Derivative, Index definidos e misturados usando herança múltipla para criar seu modelo de dados. Isso criará objetos que façam sentido e emprestem facilmente a reutilização de código.

    
por 14.11.2013 / 20:03
fonte
11

As outras respostas aqui parecem estar entrando principalmente em teoria. Então, aqui está um exemplo concreto do Python, simplificado, que eu realmente esmaguei de cabeça, o que exigiu uma quantidade razoável 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()

Bar foi escrito supondo que tivesse sua própria implementação de zeta() , o que geralmente é uma boa suposição. Uma subclasse deve substituí-la conforme apropriado para que ela faça a coisa certa. Infelizmente, os nomes eram coincidentemente os mesmos - eles faziam coisas diferentes, mas Bar agora estava chamando a implementação de Foo :

foozeta
---
barstuff
foozeta

É bastante frustrante quando não há erros lançados, o aplicativo começa a agir apenas um pouco errado, e a mudança de código que o causou (criando Bar.zeta ) não parece ser onde está o problema.

    
por 14.11.2013 / 21:00
fonte
8

Eu diria que não há problemas reais com o MI na linguagem certa . A chave é permitir estruturas de diamante, mas exigir 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.

Eu faço isso em Goiaba , uma língua na qual estou trabalhando. Uma característica do Guava é que podemos invocar a implementação de um método de um supertipo específico. Por isso, é 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 OrderedSet seu próprio toString , obteríamos um erro de compilação. Sem surpresas.

Acho que o MI é particularmente útil em coleções. Por exemplo, eu gosto de usar um tipo RandomlyEnumerableSequence para evitar declarar getEnumerator para 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 RandomAccessEnumerator para várias coleções para usar, mas ter que escrever um breve método getEnumerator ainda adiciona clichê.

Da mesma forma, o MI é útil para herdar implementações padrão de equals , hashCode e toString para coleções.

    
por 15.11.2013 / 06:45
fonte
7

Herança, múltipla ou não, não é tão importante. Se dois objetos de tipos diferentes são substituíveis, é isso que importa, mesmo que eles não estejam ligados 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 função length para obter o número de elementos em qualquer um deles.

Herança é um truque para evitar a implementação repetida do código. Se a herança salvar você e a herança múltipla poupar ainda mais trabalho em comparação à herança única, essa é toda a justificativa necessária.

Eu suspeito que algumas linguagens não implementam herança múltipla muito bem, e para os praticantes dessas linguagens, isso é o que significa herança múltipla. Mencione a herança múltipla para um programador C ++, e o que vem à mente é algo sobre problemas quando uma classe termina com duas cópias de uma base através de dois caminhos de herança diferentes e se usar virtual em uma classe base e confusão sobre como destruidores são chamados, e assim por diante.

Em muitas línguas, a herança de classe é combinada 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 namespaces lexicais, você está lidando com a importação de símbolos do namespace B para o namespace D, além de a semântica do que está acontecendo com os tipos B e D. A herança múltipla, portanto, traz problemas de conflito de símbolos. Se herdarmos de card_deck e graphic , ambos "têm" um método draw , o que significa draw o objeto resultante? Um sistema de objetos que não tem esse problema é o do Common Lisp. Talvez não coincidentemente, herança múltipla é usada em programas Lisp.

Implementado incorretamente, nada inconveniente (como herança múltipla) deve ser odiado.

    
por 14.11.2013 / 20:39
fonte
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 seus dados de classe, permitindo grande quantidade de desperdício de memória no seguinte caso:

(Meu exemplo pode não ser o melhor, mas tente entender a essência de vários espaços de memória para o mesmo propósito, foi a primeira coisa que me veio à mente: P)

Concider a DDD em que o cão da classe se estende de caninus e pet, um caninus tem uma variável que indica a quantidade de comida que deve comer (um inteiro) sob o nome dietKg, mas um animal de estimação também tem outra variável para esse propósito. sob o mesmo nome (a menos que você defina outro nome de variável, então você terá código extra codificado, que era o problema inicial que você queria evitar, para manipular e manter a integridade das variáveis bouth), então você terá dois espaços de memória para o mesmo propósito, para evitar isso, você terá que modificar seu compilador para reconhecer esse nome sob o mesmo namespace e apenas designar um espaço de memória único para esses dados, que infelizmente é possível determinar em tempo de compilação.

Você poderia, é claro, projetar uma língua para especificar que tal variável poderia já ter um espaço definido em algum outro lugar, mas no final o programador deveria especificar onde é esse espaço de memória ao qual esta variável está referenciando (e novamente código extra) ).

Acredite em mim, as pessoas implementando este pensamento realmente difícil sobre tudo isso, mas estou feliz que você tenha perguntado, a sua perspectiva é a que muda paradigmas;), e considere isso, eu não estou dizendo que é impossível (mas muitos asumptions e um compilador multifaseado devem ser implementados, e um realmente complexo), estou apenas dizendo que ele ainda não existe, se você iniciar um projeto para seu próprio compilador capaz de fazer "this" (herança múltipla) por favor me deixe sei, ficarei feliz em participar do seu time.

    
por 14.11.2013 / 17:52
fonte
-1

Já faz algum tempo que nunca me ocorreu como são totalmente diferentes algumas tarefas de programação de outras - e o quanto isso ajuda se as linguagens e os padrões usados forem adaptados ao espaço do problema.

Quando você está trabalhando sozinho ou praticamente isolado no código que escreveu, é um espaço de problema completamente diferente de herdar uma base de código de 40 pessoas na Índia que trabalhou nele por um ano antes de entregá-lo 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 estivessem aprendendo sobre (e, portanto, se apaixonando por) herança e herança múltipla ... Você poderia imaginar o que poderia estar trabalhando.

Quando você herda o código, a característica mais importante é que é compreensível e as partes são isoladas para que possam ser trabalhadas independentemente. Claro, quando você está escrevendo pela primeira vez as estruturas de código como herança múltipla, você pode economizar um pouco de duplicação e parecer se encaixar no seu humor lógico no momento, mas o próximo cara tem mais coisas para desvendar.

Toda interconexão em seu código também torna mais difícil entender e modificar partes de forma independente, duplamente com herança múltipla.

Quando você está trabalhando como parte de uma equipe, você quer direcionar o código mais simples possível que não oferece absolutamente nenhuma lógica redundante (Isso é o que DRY realmente significa, não que você não deva digitar muito apenas que nunca precisa mudar sua codifique em 2 lugares para resolver um problema!)

Existem maneiras mais simples de obter código DRY do que Múltiplas Heranças, de modo que incluí-lo em um idioma só pode abri-lo a problemas inseridos por outras pessoas que podem não estar no mesmo nível de entendimento que você. É apenas tentador se a sua linguagem é incapaz de lhe oferecer uma maneira simples / menos complexa de manter seu código DRY.

    
por 16.11.2013 / 05:22
fonte
-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 não podem não ser fornecidas e / ou mantidas sem tais 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 recompilar um módulo contendo uma classe base sem precisar recompilar cada módulo que herda dessa classe base.

  • A capacidade de um tipo herdar uma implementação de membro de uma classe pai sem que o tipo derivado tenha que reimplementá-lo

  • 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 substitui e encadeia para um membro da classe base, o membro base será invocado diretamente pelo código de encadeamento e não será chamado em nenhum outro lugar.

(*) Tipicamente 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 que classes normais podem fazer.

Se alguém deseja permitir herança múltipla generalizada, alguma outra coisa deve ceder. Se X e Y herdarem de B, ambos substituirão o mesmo membro M e cadeia pela implementação base, e se D herdar de X e Y mas não substituir M, então, dada uma instância q do tipo D, o que deve (( B) q) .M () faz? Desabilitar tal conversão violaria o axioma que diz que qualquer objeto pode ser upcast para qualquer tipo base, mas qualquer comportamento possível da invocação do elenco e do membro violaria o axioma relativo ao encadeamento de métodos. Pode-se exigir que as classes sejam carregadas apenas em combinação com a versão particular de uma classe base em que foram compiladas, mas isso é muitas vezes estranho. Ter o tempo de execução recusando-se a carregar qualquer tipo no qual qualquer ancestral possa ser alcançado por mais de uma rota pode ser viável, mas limitará muito a utilidade da herança múltipla. Permitir caminhos de herança compartilhada somente quando não existem 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 X novo e o Y novo ser compatível, mesmo que nenhum dos dois tenha feito algo que, em si e por si mesmo, deva ser uma mudança urgente.

Algumas linguagens e frameworks permitem herança múltipla, na teoria de que o que é obtido com o MI é mais importante do que o que deve ser dado para permitir isso. No entanto, os custos do MI são significativos e, em muitos casos, as interfaces fornecem 90% dos benefícios do MI a uma pequena fração do custo.

    
por 14.11.2013 / 23:14
fonte