L'argomento "orribile per la memoria" è completamente sbagliato, ma è una cattiva pratica obiettivamente ". Quando si eredita da una classe, non si ereditano solo i campi e i metodi a cui si è interessati. Invece, si eredita tutto . Ogni metodo che dichiara, anche se non è utile per te. E, soprattutto, ereditate anche tutti i suoi contratti e garantisce che la classe fornisce.
L'acronimo SOLID fornisce alcune euristiche per una buona progettazione orientata agli oggetti. Qui, il I principio di segregazione di nterface (ISP) e L iskov Sostituzione Pricampo (LSP) hanno qualcosa da dire.
L'ISP ci dice di mantenere le nostre interfacce il più piccole possibile. Ma ereditando da ArrayList
, ottieni molti, molti metodi. È significativo per get()
, remove()
, set()
(sostituisci) o add()
(inserire) un nodo figlio in un particolare indice? È sensato a ensureCapacity()
dell'elenco sottostante? Che cosa significa sort()
un nodo? Gli utenti della tua classe dovrebbero davvero ottenere un subList()
? Dal momento che non puoi nascondere metodi che non vuoi, l'unica soluzione è avere ArrayList come variabile membro e inoltrare tutti i metodi che vuoi realmente:
private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
Se vuoi davvero tutti i metodi che vedi nella documentazione, possiamo passare all'LSP. L'LSP ci dice che dobbiamo essere in grado di usare la sottoclasse ovunque sia prevista la classe genitore. Se una funzione prende un ArrayList
come parametro e invece passiamo il Node
, non si suppone che nulla cambi.
La compatibilità delle sottoclassi inizia con cose semplici come le firme dei tipi. Quando si esegue l'override di un metodo, non è possibile rendere i tipi di parametri più rigidi poiché ciò potrebbe escludere gli usi legali della classe principale. Ma questo è qualcosa che il compilatore ci controlla in Java.
Ma l'LSP funziona molto più a fondo: dobbiamo mantenere la compatibilità con tutto ciò che è promesso dalla documentazione di tutte le classi e interfacce genitore. In la loro risposta , Lynn ha trovato uno di questi casi in cui l'interfaccia List
(che hai ereditato tramite ArrayList
) garantisce come dovrebbero funzionare i metodi equals()
e hashCode()
. Per hashCode()
ti viene anche assegnato un particolare algoritmo che deve essere implementato esattamente. Supponiamo che tu abbia scritto questo Node
:
public class Node extends ArrayList<Node> {
public final int value;
public Node(int value, Node... children) {
this.value = Value;
for (Node child : children)
add(child);
}
...
}
Ciò richiede che value
non possa contribuire a hashCode()
e non può influenzare equals()
. L'interfaccia List
, che prometti di onorare ereditando da essa, richiede che new Node(0).equals(new Node(123))
sia true.
Poiché ereditare dalle classi rende troppo facile rompere accidentalmente una promessa fatta da una classe genitore, e poiché di solito espone più metodi di quelli che intendi, in genere è consigliabile che preferisca la composizione sull'ereditarietà . Se devi ereditare qualcosa, si consiglia di ereditare solo le interfacce. Se vuoi riutilizzare il comportamento di una particolare classe, puoi mantenerla come un oggetto separato in una variabile di istanza, in questo modo tutte le sue promesse e i tuoi requisiti non ti sono obbligati.
A volte, il nostro linguaggio naturale suggerisce una relazione di ereditarietà: un'auto è un veicolo. Una motocicletta è un veicolo. Devo definire le classi Car
e Motorcycle
che ereditano da una classe Vehicle
? Il design orientato agli oggetti non riguarda il mirroring del mondo reale esattamente nel nostro codice. Non possiamo facilmente codificare le ricche tassonomie del mondo reale nel nostro codice sorgente.
Un esempio è il problema della modellazione dipendente-capo. Abbiamo più Person
s, ognuno con un nome e un indirizzo. Un Employee
è un Person
e ha un Boss
. Anche un Boss
è un Person
. Quindi dovrei creare una classe Person
ereditata da Boss
e Employee
? Ora ho un problema: il capo è anche un impiegato e ha un altro superiore. Quindi sembra che Boss
debba estendere Employee
. Ma il CompanyOwner
è un Boss
ma non è un Employee
? Qualsiasi tipo di grafico ereditario verrà in qualche modo suddiviso qui.
OOP non riguarda le gerarchie, l'ereditarietà e il riutilizzo di classi esistenti, riguarda il . OOP parla di "Ho un sacco di oggetti e voglio che facciano una cosa particolare - e non mi interessa come." Ecco a cosa servono le interfacce . Se implementi l'interfaccia Iterable
per Node
perché desideri renderla iterabile, va benissimo. Se implementi l'interfaccia Collection
perché vuoi aggiungere / rimuovere nodi figli ecc., Allora va bene. Ma ereditando da un'altra classe perché capita di darti tutto ciò che non è, o almeno non a meno che tu non gli abbia dato un'attenta riflessione come descritto sopra.