Getterò il mio contributo anche se la domanda è già stata risolta abbastanza bene. Il fatto è che i programmatori tendono a pensare all'architettura in maniera tassonomica piuttosto che architettonicamente. Vale a dire, i programmatori tenderanno a cercare classificazioni di oggetti piuttosto che strutture di oggetti. Questo può probabilmente essere attribuito alla vita reale: quando incontri una persona, le pensi come
- Una donna
- Una donna bianca
- Una bella donna bianca
- Una bella donna bianca in un bel vestito.
Questo si innesca in circa un secondo quando il nostro cervello elabora la nostra visione della donna. Si finirebbe con una gerarchia di classi:
class Woman {}
class WhiteWoman extends Woman {}
class PrettyWhiteWoman extends WhiteWoman {}
class NicelyDressedPrettyWhiteWoman extends PrettyWhiteWoman {}
Ora, è abbastanza ovvio che questa architettura è molto ingenua, ma probabilmente commettiamo questi errori di progettazione laddove la gerarchia non è così facilmente definita.
Per scrivere più codice riutilizzabile, dobbiamo combattere il desiderio del nostro cervello di classificare e invece tentare di comporre. "Composizione sull'ereditarietà" come si dice.
Nel tuo esempio, stai pensando che un cavallo "è un" mammifero, e un cavallo "è un" mezzo di trasporto (cioè un'estensione). In effetti, è più corretto pensare che un cavallo "abbia una" biologia dei mammiferi e un cavallo "abbia una capacità di agire come un trasporto. Pertanto, un cavallo dovrebbe implementare i contratti di interfaccia Mammalable
e Transporter
.
Ora arriviamo a un punto di contesa su come alcune gerarchie dovrebbero essere strutturate. Ad esempio, diciamo che nella nostra farm abbiamo un sacco di FourLeggedAnimal
s, ognuno dei quali presenta un comportamento identico ai loro fratelli a quattro zampe. Nella nostra fattoria, abbiamo anche onnivori. Anche gli onnivori mostrano comportamenti simili. Il problema è che alcuni animali non sono più onnivori di quelli a quattro zampe. Non ha senso che FourLeggedAnimal
si estenda da Omnivore
o viceversa. Un esempio di questa contesa è se abbiamo sia umani che maiali nella nostra fattoria.
Invece, dobbiamo ripensare la nostra architettura. Dovremmo pensare ai nostri oggetti dall'interno piuttosto che dall'alto in basso. Invece di pensare "ciò che definisce un cavallo", dovremmo pensare "ciò che compone un cavallo". Nel contesto della nostra fattoria, questa definizione di cavallo può avere più senso (pseudo-codice basato su php):
interface Mammal {
function growHair();
}
interface Transporter {
function loadUp(bulk);
function moveOut(destination);
}
class FourLeggedAnimal {
public function walk() {
echo "Going for a walk";
}
}
class Omnivore {
public function eat(Food food) {
echo "Enjoying some food";
}
}
class Herbivore {
public function eat(Meat food) {
echo "Blech!";
}
public function eat(Veggies food) {
echo "Nom nom nom";
}
}
class Horse implements Mammal, Transporter {
private FourLeggedAnimal;
private Herbivore;
//Mammal/Transporter methods omitted
public function __construct(FourLeggedAnimal fla, Herbivore h) {
this.FourLeggedAnimal = fla;
this.Herbivore = h;
}
public function eat(Food food) {
this.Herbivore.eat(food);
}
public function walk() {
this.FourLeggedAnimal.walk();
}
}
Questo esempio è molto ingenuo e potrebbe probabilmente usare qualche miglioramento, ma spero che il concetto sia lì. Poiché ci siamo allontanati dalla mentalità "è una", la nostra architettura è più SOLIDA. Le dipendenze dei cavalli sono invertite, non dobbiamo preoccuparci delle violazioni di sostituzione di Liskov, ecc.
L'unico lato negativo di questa architettura, ed è grande, è il codice duplicato. Dobbiamo davvero avere un intero metodo walk()
per Horse
solo per mostrare il suo comportamento FourLeggedAnimal
? Se riusciamo a trovare un modo per aggirare questo problema inerente, non ci fermeremo.