Preferir composição não é apenas polimorfismo. Embora isso faça parte, e você está certo de que (pelo menos em linguagens de tipo nominal) o que as pessoas realmente querem dizer é "preferem uma combinação de composição e implementação de interface". Mas, as razões para preferir a composição (em muitas circunstâncias) são profundas.
Polimorfismo é sobre uma coisa que se comporta de várias maneiras. Portanto, os genéricos / modelos são um recurso "polimórfico", na medida em que permitem que um único pedaço de código altere seu comportamento com os tipos. De fato, esse tipo de polimorfismo é realmente o melhor comportamento e geralmente é chamado de polimorfismo paramétrico, porque a variação é definida por um parâmetro.
Muitos idiomas fornecem uma forma de polimorfismo chamado "sobrecarga" ou polimorfismo ad hoc, em que vários procedimentos com o mesmo nome são definidos de maneira ad hoc e onde um é escolhido pelo idioma (talvez o mais específico). Esse é o tipo de polimorfismo menos bem comportado, já que nada conecta o comportamento dos dois procedimentos, exceto a convenção desenvolvida.
Um terceiro tipo de polimorfismo é o subtipo de polimorfismo . Aqui, um procedimento definido em um determinado tipo também pode funcionar em uma família inteira de "subtipos" desse tipo. Quando você implementa uma interface ou estende uma classe, geralmente declara sua intenção de criar um subtipo. Os subtipos verdadeiros são regidos pelo Princípio da Substituição de Liskov, que diz que se você pode provar algo sobre todos os objetos em um supertipo, pode provar isso sobre todas as instâncias em um subtipo. A vida fica perigosa, já que em linguagens como C ++ e Java, as pessoas geralmente têm suposições não-impostas e muitas vezes não documentadas sobre classes que podem ou não ser verdadeiras sobre suas subclasses. Ou seja, o código é escrito como se fosse mais provável do que realmente é, o que produz uma série de problemas quando você subtipo descuidado.
A herança é realmente independente do polimorfismo. Dada alguma coisa "T" que tem uma referência a si mesma, a herança acontece quando você cria uma nova coisa "S" a partir de "T" substituindo a referência de "T" a si mesma por uma referência a "S". Essa definição é intencionalmente vaga, já que a herança pode ocorrer em muitas situações, mas a mais comum é a subclasse de um objeto que tem o efeito de substituir o this
ponteiro chamado por funções virtuais pelo this
ponteiro do subtipo.
A herança é uma coisa perigosa, como todas as coisas muito poderosas que a herança tem o poder de causar estragos. Por exemplo, suponha que você substitua um método ao herdar de alguma classe: tudo está bem até que algum outro método dessa classe assuma que o método que você herdou se comporte de uma certa maneira, afinal, foi assim que o autor da classe original a projetou. . Você pode se proteger parcialmente contra isso declarando todos os métodos chamados por outro de seus métodos como privados ou não virtuais (final), a menos que sejam projetados para serem substituídos. Mesmo isso nem sempre é bom o suficiente. Às vezes, você pode ver algo assim (em pseudo Java, espero que seja legível para usuários de C ++ e C #)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
você acha isso adorável e tem seu próprio jeito de fazer "coisas", mas usa a herança para adquirir a capacidade de fazer "mais",
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
E tudo está bem e bem. WayOfDoingUsefulThings
foi projetado de tal maneira que a substituição de um método não altera a semântica de nenhum outro ... exceto espere, não, não foi. Parece que sim, mas doThings
mudou o estado mutável que importava. Portanto, mesmo que não tenha chamado funções de substituição,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
agora faz algo diferente do esperado quando você passa a MyUsefulThings
. O que é pior, você pode nem saber que WayOfDoingUsefulThings
fez essas promessas. Talvez dealWithStuff
venha da mesma biblioteca WayOfDoingUsefulThings
e getStuff()
nem seja exportada pela biblioteca (pense nas classes de amigos em C ++). Pior ainda, você derrotou as verificações estáticas da linguagem sem perceber: dealWithStuff
tomou uma decisão WayOfDoingUsefulThings
apenas para garantir que ela tivesse uma getStuff()
função que se comportasse de uma certa maneira.
Usando composição
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
devolve a segurança do tipo estático. Em geral, a composição é mais fácil de usar e mais segura que a herança ao implementar a subtipagem. Ele também permite que você substitua os métodos finais, o que significa que você deve se sentir à vontade para declarar tudo final / não virtual, exceto nas interfaces a grande maioria das vezes.
Em um mundo melhor, os idiomas inseriam automaticamente o clichê com uma delegation
palavra - chave. A maioria não, então a desvantagem são as classes maiores. No entanto, você pode fazer com que o seu IDE grave a instância de delegação para você.
Agora, a vida não é apenas sobre polimorfismo. Você não pode precisar subtipo o tempo todo. O objetivo do polimorfismo é geralmente a reutilização de código, mas não é a única maneira de atingir esse objetivo. Muitas vezes, faz sentido usar a composição, sem subtipo de polimorfismo, como uma maneira de gerenciar a funcionalidade.
Além disso, a herança comportamental tem seus usos. É uma das idéias mais poderosas da ciência da computação. É justo que, na maioria das vezes, bons aplicativos de POO possam ser gravados usando apenas usando herança e composições de interface. Os dois princípios
- Proibir herança ou design
- Preferir composição
são um bom guia pelos motivos acima e não incorrem em custos substanciais.