Gostaria de receber alguns esclarecimentos sobre como implementar adequadamente a 'Composição sobre Herança'. Acho que tenho uma noção da teoria, mas na prática tenho dificuldade em entender como isso não leva à duplicação de código.
Tenho o seguinte exemplo (é Java): suponha que temos uma classe animal abstrata:
abstract class Animal {
protected void eat() {
// Common eating implementation
}
protected void sleep() {
// Common sleeping implementation
}
}
E queremos construir animais voadores e nadadores. Entendo que a melhor maneira de fazer isso para seguirmos o LSP é com interfaces para cada um:
interface Flyer {
void fly();
}
interface Swimmer {
void swim();
}
Então teríamos
class Salmon extends Animal implements Swimmer {
@Override
public void swim() {
// Swim implementation
}
}
class Sparrow extends Animal implements Flyer {
@Override
public void fly() {
// Fly implementation
}
}
Mas então temos um novo requisito para Magpie
que voe da mesma forma que um pardal. Criaríamos a classe, não muito diferente de Sparrow
:
class Magpie extends Animal implements Flyer {
@Override
public void fly() {
// Same exact fly implementation as Sparrow.fly
}
}
Imagine, para o propósito deste exercício, que a implementação fly é muito complexa com integração de banco de dados, registro, etc. - isso leva a uma carga de código duplicado e se adicionarmos mais pássaros ou peixes, precisaremos duplicar o código ainda avançar.
Houve também a ideia de ter uma classe abstrata para o mesmo tipo de panfletos como
abstract class FlyingBird extends Animal implements Flyer
e temos a implementação comum lá, mas e se precisarmos criar alguns deles e houver um animal que precise estender dois deles? É uma ladeira escorregadia...
Existe alguma maneira de evitar isso? Ou estou errando o alvo em algum lugar?
Na composição, que sou a favor da herança para compartilhar código por vários motivos, você pode tentar compartilhar código injetando uma implementação comum.
No seu exemplo, você poderia ter um impl Flyer que pode ser compartilhado entre Sparrow e Magpie. Nomeá-lo pode ser um pouco difícil - recomendo usar o princípio "usar antes de reutilizar" e, portanto, apenas dê um nome bom o suficiente e refatore mais tarde, se necessário.
Então, por exemplo, você poderia ter uma implementação SmallBirdFlyer, injetá-la nas implementações Sparrow e Magpie e, em seguida, usar a delegação de Sparrow e Magpie para esse impl. Ou, em vez de fazer com que Sparrow e Magpie implementem Flyer diretamente, você pode fazer com que eles retornem um Flyer por meio de um método, como
getFlyer()
, e então Sparrow e Magpie poderiam retornar diretamente o impl SmallBirdFlyer lá.Aqui está um exemplo usando delegação:
Eu penso desta maneira - em vez de reduzir a funcionalidade do nível superior e depois encontrar maneiras de personalizar nos níveis inferiores, crie a funcionalidade comum para uso no nível superior.