Por que usar inheritance em tudo?

Sei que a questão já foi discutida antes , mas parece sempre pressupor que a inheritance é pelo menos às vezes preferível à composição. Eu gostaria de desafiar essa suposição na esperança de ganhar algum entendimento.

Minha pergunta é a seguinte: como você pode realizar qualquer coisa com a composição de objects com inheritance clássica e como a inheritance clássica é frequentemente abusada [1] e como a composição de objects oferece flexibilidade para alterar o tempo de execução do object delegado, por que você usaria? inheritance clássica?

Eu meio que entendo porque você recomendaria inheritance em algumas linguagens como Java e C ++ que não oferecem syntax conveniente para delegação. Nessas linguagens, você pode economizar muita digitação usando inheritance sempre que não estiver claramente incorreto para fazer isso. Mas outras linguagens como Objective C e Ruby oferecem inheritance clássica e syntax muito conveniente para delegação. A linguagem de programação Go é a única linguagem que, até onde sei, decidiu que a inheritance clássica é mais problemática do que vale a pena e suporta apenas a delegação para reutilização de código.

Outra maneira de declarar minha pergunta é esta: Mesmo que você saiba que a inheritance clássica não é incorreta para implementar um determinado modelo, essa razão é suficiente para usá-lo em vez de composição?

[1] Muitas pessoas usam inheritance clássica para alcançar o polymorphism, em vez de permitir que suas classs implementem uma interface. O objective da inheritance é a reutilização de código, não o polymorphism. Além disso, algumas pessoas usam a inheritance para modelar sua compreensão intuitiva de um relacionamento “é-um” que muitas vezes pode ser problemático .

Atualizar

Eu só quero esclarecer o que quero dizer exatamente quando falo de inheritance:

Eu estou falando sobre o tipo de inheritance em que uma class herda de uma class base parcialmente ou totalmente implementada . Não estou falando de herdar de uma class base puramente abstrata, o que equivale à implementação de uma interface, com a qual, para o registro, não estou argumentando contra.

Atualização 2

Eu entendo que a inheritance é a única maneira de alcançar o polymorphism i C ++. Nesse caso, é óbvio porque você deve usá-lo. Então, minha pergunta é limitada a linguagens como Java ou Ruby, que oferecem maneiras distintas de alcançar o polymorphism (interfaces e tipping duck, respectivamente).

Se você delega tudo o que você não explicitamente substituiu para algum outro object que implementa a mesma interface (o object “base”), então você basicamente tem inheritance Greenspunned no topo da composição, mas (na maioria das linguagens) com muito mais verbosidade e clichê. O propósito de usar composição em vez de inheritance é para que você possa apenas delegar os comportamentos que deseja delegar.

Se você quiser que o object use todo o comportamento da class base a menos que seja explicitamente substituído, a inheritance é a maneira mais simples, menos detalhada e mais direta de expressá-la.

O objective da inheritance é a reutilização de código, não o polymorphism.

Este é o seu erro fundamental. Quase exatamente o oposto é verdadeiro. O objective principal da inheritance (pública) é modelar os relacionamentos entre as classs em questão. O polymorphism é uma grande parte disso.

Quando usada corretamente, a inheritance não é reutilizar o código existente. Pelo contrário, trata-se de ser usado pelo código existente. Ou seja, se você tiver um código existente que possa trabalhar com a class base existente, ao derivar uma nova class da class base existente, outro código também poderá funcionar automaticamente com sua nova class derivada.

É possível usar inheritance para reutilização de código, mas quando / se você fizer isso, normalmente deveria ser inheritance privada e não inheritance pública. Se o idioma que você está usando oferecer suporte à delegação, é muito provável que você tenha poucos motivos para usar a inheritance particular. OTOH, inheritance privada suporta algumas coisas que a delegação (normalmente) não suporta. Em particular, mesmo que o polymorphism seja uma preocupação decididamente secundária nesse caso, ele ainda pode ser uma preocupação – isto é, com inheritance privada você pode começar de uma class base que é quase o que você quer, e (assumindo que isso permita) replace o partes que não estão bem.

Com a delegação, sua única opção real é usar a class existente exatamente como está. Se não fizer exatamente o que você quer, sua única opção real é ignorar completamente essa funcionalidade e reimplementá-la do zero. Em alguns casos, isso não é perda, mas em outros é bastante substancial. Se outras partes da class base usam a function polimórfica, a inheritance privada permite replace apenas a function polimórfica e as outras partes usarão a function substituída. Com a delegação, não é possível conectar facilmente sua nova funcionalidade para que outras partes da class base existente usem o que você substituiu.

A principal razão para usar a inheritance não é como uma forma de composição – é para que você possa obter um comportamento polimórfico. Se você não precisa de polymorphism, provavelmente não deveria estar usando inheritance, pelo menos em C ++.

A inheritance deve ser preferida se:

  1. Você precisa expor toda a API da class que você estende (com a delegação, você precisará escrever muitos methods de delegação) e seu idioma não oferece uma maneira simples de dizer “delegar todos os methods desconhecidos para”.
  2. Você precisa acessar campos / methods protegidos para idiomas que não têm conceito de “amigos”
  3. As vantagens da delegação são um pouco reduzidas se a sua linguagem permitir multi-inheritance
  4. Você normalmente não precisa de delegação se sua linguagem permitir herdar dinamicamente de uma class ou mesmo de uma instância em tempo de execução. Você não precisa disso se puder controlar quais methods são expostos (e como eles são expostos) ao mesmo tempo.

Minha conclusão: Delegação é uma solução para um bug em uma linguagem de programação.

Todos sabem que o polymorphism é uma grande vantagem da inheritance. Outro benefício que eu acho em inheritance é que ajuda a criar réplica do mundo real. Por exemplo, no sistema de pagamento por pagamento, lidamos com gerentes de desenvolvimento, garotos de escritório, etc., se herdamos todas essas classs com Super Employee. Isso torna nosso programa mais compreensível no contexto do mundo real que todas essas classs são basicamente funcionários. E mais uma coisa, as classs não apenas contêm methods, elas também contêm atributos. Portanto, se contivermos atributos genéricos para funcionário na class Funcionário, como a idade do número da previdência social etc., ele fornecerá maior reutilização de código e clareza conceitual e, claro, polymorphism. No entanto, ao usar as coisas de inheritance, devemos ter em mente o princípio do design básico: “Identifique os aspectos da sua aplicação que variam e os separe dos aspectos que mudam”. Você nunca deve implementar esses aspectos do aplicativo que mudam por inheritance, em vez disso, use composição. E para aqueles aspectos que não são mutáveis, você deve usar a inheritance de curso, se uma relação óbvia “é uma” mentir.

Eu sempre penso duas vezes antes de usar a inheritance, pois ela pode ficar complicada rapidamente. Dito isto, há muitos casos em que simplesmente produz o código mais elegante.

Interfaces só definem o que um object pode fazer e não como. Portanto, em termos simples, as interfaces são apenas contratos. Todos os objects que implementam a interface terão que definir sua própria implementação do contrato. No mundo prático, isso lhe dá separation of concern . Imagine-se escrevendo um aplicativo que precisa lidar com vários objects que você não os conhece de antemão, ainda precisa lidar com eles, a única coisa que você sabe é quais são as diferentes coisas que esses objects devem fazer. Então você vai definir uma interface e mencionar todas as operações no contrato. Agora você vai escrever seu aplicativo nessa interface. Mais tarde, quem quiser aproveitar o seu código ou aplicativo terá que implementar a interface no object para fazê-lo funcionar com o seu sistema. Sua interface forçará seu object a definir como cada operação definida no contrato deve ser feita. Dessa forma, qualquer pessoa pode escrever objects que implementam sua interface, para que eles se adaptem perfeitamente ao seu sistema e tudo o que você sabe é o que precisa ser feito e é o object que precisa definir como isso é feito.

No desenvolvimento do mundo real, essa prática é geralmente conhecida como Programming to Interface and not to Implementation .

Interfaces são apenas contratos ou assinaturas e eles não sabem nada sobre implementações.

Codificar contra interface significa que o código do cliente sempre contém um object Interface que é fornecido por uma fábrica. Qualquer instância retornada pela fábrica seria do tipo Interface que qualquer class candidata de fábrica deve ter implementado. Desta forma, o programa cliente não está preocupado com a implementação e a assinatura da interface determina o que todas as operações podem ser feitas. Isso pode ser usado para alterar o comportamento de um programa em tempo de execução. Ele também ajuda você a escrever programas muito melhores do ponto de vista de manutenção.

Aqui está um exemplo básico para você.

 public enum Language { English, German, Spanish } public class SpeakerFactory { public static ISpeaker CreateSpeaker(Language language) { switch (language) { case Language.English: return new EnglishSpeaker(); case Language.German: return new GermanSpeaker(); case Language.Spanish: return new SpanishSpeaker(); default: throw new ApplicationException("No speaker can speak such language"); } } } [STAThread] static void Main() { //This is your client code. ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English); speaker.Speak(); Console.ReadLine(); } public interface ISpeaker { void Speak(); } public class EnglishSpeaker : ISpeaker { public EnglishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak English."); } #endregion } public class GermanSpeaker : ISpeaker { public GermanSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak German."); } #endregion } public class SpanishSpeaker : ISpeaker { public SpanishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak Spanish."); } #endregion } 

alt text http://ruchitsurati.net/myfiles/interface.png

E quanto ao padrão de método de modelo? Digamos que você tenha uma class base com muitos pontos para políticas personalizáveis, mas um padrão de estratégia não faz sentido por pelo menos um dos seguintes motivos:

  1. As políticas personalizáveis ​​precisam saber sobre a class base, só podem ser usadas com a class base e não fazem sentido em nenhum outro contexto. Em vez disso, usar estratégia é possível, mas uma PITA, porque a class base e a class de política precisam ter referências umas às outras.

  2. As políticas são acopladas umas às outras na medida em que não faria sentido misturá-las e combiná-las livremente. Eles só fazem sentido em um subconjunto muito limitado de todas as combinações possíveis.

Você escreveu:

[1] Muitas pessoas usam inheritance clássica para alcançar o polymorphism, em vez de permitir que suas classs implementem uma interface. O objective da inheritance é a reutilização de código, não o polymorphism. Além disso, algumas pessoas usam a inheritance para modelar sua compreensão intuitiva de um relacionamento “é-um” que muitas vezes pode ser problemático.

Na maioria das linguagens, a linha entre ‘implementar uma interface’ e ‘derivar uma class de outra’ é muito pequena. De fato, em linguagens como C ++, se você estiver derivando uma class B de uma class A, e A for uma class que consiste apenas em methods virtuais puros, você está implementando uma interface.

Herança é sobre reutilização de interface , não reutilização de implementação . Não se trata de reutilização de código, como você escreveu acima.

A inheritance, como você aponta corretamente, pretende modelar um relacionamento IS-A (o fato de muitas pessoas errarem não tem nada a ver com inheritance em si). Você também pode dizer ‘BEHAVES-LIKE-A’. No entanto, só porque algo tem um relacionamento IS-A com outra coisa, não significa que ele usa o mesmo código (ou mesmo similar) para preencher esse relacionamento.

Compare este exemplo de C ++ que implementa maneiras diferentes de enviar dados; duas classs usam inheritance (pública) para que possam acessar polimorficamente:

 struct Output { virtual bool readyToWrite() const = 0; virtual void write(const char *data, size_t len) = 0; }; struct NetworkOutput : public Output { NetworkOutput(const char *host, unsigned short port); bool readyToWrite(); void write(const char *data, size_t len); }; struct FileOutput : public Output { FileOutput(const char *fileName); bool readyToWrite(); void write(const char *data, size_t len); }; 

Agora imagine se isso fosse Java. ‘Output’ não foi struct, mas uma ‘interface’. Pode ser chamado de “gravável”. Em vez de “Saída pública”, você diria “implementa Gravável”. Qual é a diferença no que diz respeito ao design?

Nenhum.

A principal utilidade da inheritance clássica é se você tiver várias classs relacionadas que terão lógica idêntica para methods que operam em variables ​​/ propriedades de instâncias.

Existem realmente 3 maneiras de lidar com isso:

  1. Herança.
  2. Duplique o código ( cheiro de código “Código duplicado”).
  3. Mova a lógica para outra class (código cheira “Lazy Class”, “Middle Man”, “Message Chains” e / ou “Inappropriate Intimacy”).

Agora, pode haver abuso de inheritance. Por exemplo, o Java possui as classs InputStream e OutputStream . Subclasss destes são usados ​​para ler / gravar arquivos, sockets, arrays, strings, e vários são usados ​​para envolver outros streams de input / saída. Com base no que eles fazem, devem ter sido interfaces e não classs.

Uma das formas mais úteis que vejo para usar a inheritance está nos objects da GUI.

Quando você perguntou:

Mesmo que você saiba que a inheritance clássica não é incorreta para implementar um determinado modelo, essa razão é suficiente para usá-lo em vez de composição?

A resposta é não. Se o modelo está incorreto (usando inheritance), então é errado usar, não importa o quê.

Aqui estão alguns problemas com inheritance que eu vi:

  1. Sempre tendo que testar o tipo de tempo de execução de pointers de class derivados para ver se eles podem ser lançados (ou para baixo também).
  2. Este ‘teste’ pode ser alcançado de várias maneiras. Você pode ter algum tipo de método virtual que retorna um identificador de class. Ou se você não tiver que implementar o RTTI (Identificação do tipo de tempo de execução) (Pelo menos em c / c ++), o que pode causar um impacto no desempenho.
  3. os tipos de class que não conseguem ser ‘lançados’ podem ser potencialmente problemáticos.
  4. Há muitas maneiras de transmitir seu tipo de class para cima e para baixo na tree de inheritance.

Algo totalmente não OOP mas ainda assim, composição normalmente significa um cache-miss extra. Isso depende, mas ter os dados mais próximos é uma vantagem.

Geralmente, eu me recuso a entrar em algumas lutas religiosas, usando seu próprio julgamento e estilo é o melhor que você pode obter.