Vários casos de uso de herança

15

O Java omite a herança múltipla com o objetivo de evitar o objetivo do projeto de manter a linguagem simples .

Gostaria de saber se Java (com seu ecossistema) é realmente "simples". Python não é complexo e tem herança múltipla. Então, sem ser muito subjetivo, minha pergunta é ...

Quais são os padrões de problemas típicos que se beneficiam de um código projetado para fazer uso pesado de várias heranças

codificador de árvore
fonte
8
O Java omite muitas coisas perfeitamente boas por muito pouco motivo. Eu não esperaria uma justificativa decente para o MI.
DeadMG
2
A herança múltipla do Python é definitivamente o território aqui-seja-dragões . O fato de ele usar a resolução de nomes em profundidade, da esquerda para a direita, tem problemas significativos em termos de manutenção e compreensão. Embora possa ser útil em hierarquias rasas de classe, pode ser incrivelmente contra-intuitivo em profundas.
Mark Booth
Acho que o motivo pelo qual o Java não contém herança múltipla é que os desenvolvedores do Java queriam que sua linguagem fosse fácil de aprender. A herança múltipla, embora incrivelmente poderosa em alguns casos, é difícil de entender e ainda mais difícil de ser usada com bons resultados; não é com isso que você gostaria de confrontar um calouro de programação. Quero dizer: como você explica a herança virtual para alguém que está lutando com o próprio conceito de herança? E, como a herança múltipla também não é exatamente trivial do lado dos implementadores, é provável que o desenvolvedor de Java, embora a omissão seja uma vitória.
cmaster - restabelece monica
Java é nominalmente digitado. Python não é. Isso torna a herança múltipla muito mais fácil de implementar e entender em Python.
Jules

Respostas:

11

Prós:

  1. Às vezes, permite modelagem mais óbvia de um problema do que outras maneiras de modelá-lo.
  2. Se os diferentes pais têm uma finalidade ortogonal, isso pode permitir algum tipo de composição

Contras:

  1. Se os pais diferentes não têm um objetivo ortogonal, torna o tipo difícil de entender.
  2. Não é fácil entender como isso é implementado em um idioma (qualquer idioma).

No C ++, um bom exemplo de herança múltipla usada para compor recursos ortogonais é quando você usa o CRTP para, por exemplo, configurar um sistema de componentes para um jogo.

Comecei a escrever um exemplo, mas acho que vale a pena olhar um exemplo do mundo real. Algum código do Ogre3D usa herança múltipla de uma maneira agradável e muito intuitiva. Por exemplo, a classe Mesh herda de Resources e AnimationContainer. Os recursos expõem a interface comum a todos os recursos e o AnimationContainer expõe a interface específica para manipular um conjunto de animações. Eles não estão relacionados, portanto, é fácil pensar em um Mesh como um recurso que, além disso, pode conter um conjunto de animações. Parece natural, não é?

Você pode ver outros exemplos nesta biblioteca , como a maneira como a alocação de memória é gerenciada de maneira detalhada, tornando as classes herdadas das variantes de uma classe CRTP sobrecarregando new e delete.

Como dito, os principais problemas com herança múltipla surgem da mistura de conceitos relacionados. Isso faz com que a linguagem precise definir implementações complexas (veja como o C ++ permite lidar com o problema do diamante ...) e o usuário não tendo certeza do que está acontecendo nessa implementação. Por exemplo, leia este artigo explicando como ele é implementado em C ++ .

Removê-lo do idioma ajuda a evitar pessoas que não sabem como o idioma é forçado a tornar as coisas ruins. Mas isso força a pensar de uma maneira que, às vezes, não parece natural, mesmo que seja um caso extremo, acontece com mais frequência do que você imagina.

Klaim
fonte
Eu realmente aprecio se você adornada sua resposta com um problema exemplo - que fará com que termos como "propósito ortogonal" mais clara - mas graças
treecoder
Ok, deixe-me tentar adicionar algo.
Klaim
Ogre3D não é um lugar onde eu procuraria inspiração em design - você viu a infecção pelo Singleton?
DeadMG
Primeiro, o herdeiro singleton não é realmente um singleton, a construção e a destruição são explícitas. Em seguida, o Ogre é uma camada sobre um sistema de hardware (ou o driver gráfico, se você preferir). Isso significa que deve haver apenas uma representação exclusiva para interfaces do sistema (como Raiz ou outras). Eles podem remover alguns singleton, mas esse não é o ponto aqui. Eu voluntariamente evitei apontar isso para evitar uma discussão sobre trolls, então, por favor, veja os exemplos que eu apontei. O uso de Singleton pode não ser perfeito, mas é claramente útil na prática (mas apenas para o tipo de sistema, não tudo).
Klaim
4

Existe um conceito chamado mixins que é muito usado em linguagens mais dinâmicas. A herança múltipla é uma maneira pela qual os mixins podem ser suportados por um idioma. Mixins são geralmente usados ​​como uma maneira de uma classe acumular diferentes partes de funcionalidade. Sem herança múltipla, é necessário usar agregação / delegação para obter o comportamento do tipo mixin com uma classe, que é um pouco mais pesada em sintaxe.

RationalGeek
fonte
+1, este é realmente um bom motivo para ter herança múltipla. Mixins carregam conotações adicionais ("essa classe não deve ser usada como autônoma")
ashes999
2

Eu acho que a escolha é baseada principalmente em questões devido ao problema do diamante .

Além disso, muitas vezes é possível contornar o uso de herança múltipla por delegação ou outros meios.

Não tenho certeza do significado da sua última pergunta. Mas se for "em que casos a herança múltipla é útil?", Em todos os casos em que você gostaria de ter um objeto A com funcionalidades dos objetos B e C, basicamente.

dagnelies
fonte
2

Não vou me aprofundar muito aqui, mas você certamente pode entender a herança múltipla em python através do seguinte link http://docs.python.org/release/1.5.1p1/tut/multiple.html :

A única regra necessária para explicar a semântica é a regra de resolução usada para referências de atributos de classe. Isso é profundidade primeiro, da esquerda para a direita. Portanto, se um atributo não for encontrado em DerivedClassName, ele será pesquisado na Base1, depois (recursivamente) nas classes base da Base1, e somente se não for encontrado lá, será pesquisado na Base2 e assim por diante.

...

É claro que o uso indiscriminado de herança múltipla é um pesadelo de manutenção, dada a dependência do Python de convenções para evitar conflitos acidentais de nomes. Um problema conhecido com herança múltipla é uma classe derivada de duas classes que possuem uma classe base comum. Embora seja fácil descobrir o que acontece nesse caso (a instância terá uma única cópia de `` variáveis ​​de instância '' ou atributos de dados usados ​​pela classe base comum), não está claro que essas semânticas sejam de alguma forma útil.

Este é apenas um pequeno parágrafo, mas grande o suficiente para esclarecer as dúvidas, eu acho.

Pankaj Upadhyay
fonte
1

Um local em que a herança múltipla seria útil é uma situação em que uma classe implementa várias interfaces, mas você gostaria de ter alguma funcionalidade padrão incorporada a cada interface. Isso é útil se a maioria das classes que implementam alguma interface deseja fazer algo da mesma maneira, mas ocasionalmente você precisa fazer algo diferente. Você pode ter cada classe com a mesma implementação, mas faz mais sentido empurrá-la para um único local.

KeithB
fonte
1
Isso exigiria herança múltipla generalizada ou simplesmente um meio pelo qual uma interface possa especificar comportamentos padrão para métodos não implementados? Se as interfaces pudessem especificar implementações padrão apenas para os métodos que elas mesmas implementam (em oposição às que herdam de outras interfaces), esse recurso evitaria completamente os problemas de diamante duplo que dificultam a herança múltipla.
Supercat 24/12
1

Quais são os padrões de problemas típicos que se beneficiam de um código projetado para fazer uso pesado de várias heranças?

Este é apenas um exemplo, mas acho inestimável para melhorar a segurança e atenuar as tentações de aplicar mudanças em cascata em todos os chamadores ou subclasses.

Onde eu achei várias heranças incrivelmente úteis, mesmo para as interfaces mais abstratas e sem estado, é o idioma da interface não virtual (NVI) no C ++.

Eles nem são realmente classes básicas abstratas, mas interfaces que têm apenas um pouco de implementação para impor os aspectos universais de seus contratos, pois não estão realmente estreitando a generalidade do contrato, mas sim aplicando-o melhor. .

Exemplo simples (alguns podem verificar se um identificador de arquivo passado está aberto ou algo parecido):

// Non-virtual interface (public methods are nonvirtual/final).
// Since these are modeling the concept of "interface", not ABC,
// multiple will often be inherited ("implemented") by a subclass.
class SomeInterface
{
public:
    // Pre: x should always be greater than or equal to zero.
    void f(int x) /*final*/
    {
        // Make sure x is actually greater than or equal to zero
        // to meet the necessary pre-conditions of this function.
        assert(x >= 0);

        // Call the overridden function in the subtype.
        f_impl(x);
    }

protected:
    // Overridden by a boatload of subtypes which implement
    // this non-virtual interface.
    virtual void f_impl(int x) = 0;
};

Nesse caso, talvez fseja chamado por mil lugares na base de código, enquantof_impl é substituído por cem subclasses.

Seria difícil fazer esse tipo de verificação de segurança em todos os 1000 locais que ligam fou nos 100 locais que substituemf_impl .

Ao fazer esse ponto de entrada para a funcionalidade não virtual, ele me fornece um lugar central para executar essa verificação. E essa verificação não está reduzindo a abstração nem um pouco, pois está simplesmente afirmando uma pré-condição necessária para chamar essa função. Em certo sentido, é possível reforçar o contrato fornecido pela interface e aliviar o ônus de verificar a xentrada para garantir que ela esteja em conformidade com as pré-condições válidas em todos os 100 locais que a substituem.

É algo que eu gostaria que toda linguagem tivesse, e também desejasse, mesmo em C ++, que fosse um pouco mais de um conceito nativo (por exemplo: não exigindo que definíssemos uma função separada para substituir).

Isso é extremamente útil se você não fez isso assertcom antecedência e percebeu que precisava disso mais tarde, quando alguns lugares aleatórios na base de código encontravam valores negativos sendo passados ​​para f.


fonte
0

Primeiro: várias cópias da classe base (um problema em C ++) e forte acoplamento entre as classes base e derivadas.

Segundo: herança múltipla de interfaces abstratas

quant_dev
fonte
você está sugerindo que não é útil em nenhum contexto? E que tudo pode ser projetado / codificado convenientemente sem ele? Também, por favor, elabore o segundo ponto.
treecoder