Preferire la composizione non riguarda solo il polimorfismo. Anche se questo fa parte di questo, e hai ragione (almeno nei linguaggi nominati tipicamente) ciò che la gente intende veramente è "preferire una combinazione di composizione e implementazione dell'interfaccia". Ma i motivi per preferire la composizione (in molte circostanze) sono profondi.
Il polimorfismo riguarda una cosa che si comporta in diversi modi. Pertanto, generici / modelli sono una funzionalità "polimorfica" in quanto consentono a un singolo pezzo di codice di variare il suo comportamento con i tipi. In effetti, questo tipo di polimorfismo è davvero il comportamento migliore e viene generalmente definito polimorfismo parametrico perché la variazione è definita da un parametro.
Molte lingue forniscono una forma di polimorfismo chiamata "sovraccarico" o polimorfismo ad hoc in cui più procedure con lo stesso nome sono definite in un modo ad hoc e in cui si è scelti dal linguaggio (forse il più specifico). Questo è il tipo di polimorfismo meno ben educato, dal momento che nulla collega il comportamento delle due procedure tranne la convenzione sviluppata.
Un terzo tipo di polimorfismo è il polimorfismo del sottotipo . Qui una procedura definita su un determinato tipo, può funzionare anche su un'intera famiglia di "sottotipi" di quel tipo. Quando si implementa un'interfaccia o si estende una classe, in genere si dichiara l'intenzione di creare un sottotipo. I veri sottotipi sono regolati dal Principio di sostituzione di Liskov, che dice che se puoi provare qualcosa su tutti gli oggetti in un supertipo puoi provarlo su tutte le istanze in un sottotipo. La vita diventa pericolosa, tuttavia, poiché in linguaggi come C ++ e Java, le persone generalmente hanno ipotesi non rinforzate e spesso prive di documenti sulle classi che possono o meno essere vere riguardo alle loro sottoclassi. Cioè, il codice è scritto come se sia dimostrabile più di quello che è realmente, il che produce una serie di problemi quando si sottotipa con noncuranza.
L'ereditarietà è in realtà indipendente dal polimorfismo. Dato qualcosa "T" che ha un riferimento a se stesso, l'ereditarietà accade quando si crea una nuova cosa "S" da "T" sostituendo il riferimento "T" a se stesso con un riferimento a "S". Tale definizione è intenzionalmente vaga, poiché l'ereditarietà può verificarsi in molte situazioni, ma la più comune è la sottoclasse di un oggetto che ha l'effetto di sostituire il this
puntatore chiamato da funzioni virtuali con il this
puntatore al sottotipo.
L'ereditarietà è pericolosa come tutte le cose molto potenti che l'eredità ha il potere di provocare il caos. Ad esempio, supponete di avere la precedenza su un metodo quando ereditate da una classe: tutto va bene fino a quando un altro metodo di quella classe assume il metodo che ereditate per comportarvi in un certo modo, dopo tutto è come l'autore della classe originale lo ha progettato . Puoi parzialmente proteggerti da questo dichiarando tutti i metodi chiamati da un altro dei tuoi metodi privati o non virtuali (final), a meno che non siano progettati per essere sovrascritti. Anche questo però non è sempre abbastanza buono. A volte potresti vedere qualcosa del genere (in pseudo Java, si spera sia leggibile dagli utenti 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
...
}
}
pensi che sia adorabile e hai il tuo modo di fare "cose", ma usi l'eredità per acquisire la capacità di fare "più cose",
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
E tutto va bene. WayOfDoingUsefulThings
è stato progettato in modo tale che la sostituzione di un metodo non cambi la semantica di nessun altro ... tranne aspettare, no, non lo era. Sembra che fosse, ma ha doThings
cambiato lo stato mutevole che contava. Quindi, anche se non ha chiamato alcuna funzione abilitata per l'override,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
ora fa qualcosa di diverso dal previsto quando lo passi a MyUsefulThings
. Quel che è peggio, potresti anche non sapere che ha WayOfDoingUsefulThings
fatto quelle promesse. Forse dealWithStuff
viene dalla stessa libreria WayOfDoingUsefulThings
e getStuff()
non viene nemmeno esportato dalla libreria (pensate alle classi di amici in C ++). Peggio ancora, hai sconfitto i controlli statici della lingua senza rendertene conto: hai dealWithStuff
preso un WayOfDoingUsefulThings
giusto per assicurarti che avrebbe una getStuff()
funzione che si comportava in un certo modo.
Usando la composizione
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
riporta la sicurezza di tipo statico. In generale, la composizione è più facile da usare e più sicura dell'eredità durante l'implementazione del sottotipo. Ti consente anche di ignorare i metodi finali, il che significa che dovresti sentirti libero di dichiarare tutto finale / non virtuale tranne che nelle interfacce la maggior parte delle volte.
In un mondo migliore, le lingue inseriscono automaticamente la caldaia con una delegation
parola chiave. La maggior parte no, quindi un aspetto negativo sono le classi più grandi. Tuttavia, puoi ottenere il tuo IDE per scrivere l'istanza di delega per te.
Ora, la vita non riguarda solo il polimorfismo. Non è necessario sottotipare continuamente. L'obiettivo del polimorfismo è generalmente il riutilizzo del codice, ma non è l'unico modo per raggiungere tale obiettivo. Spesso ha senso usare la composizione, senza polimorfismo di sottotipo, come modo di gestire la funzionalità.
Inoltre, l'eredità comportamentale ha i suoi usi. È una delle idee più potenti nell'informatica. È solo che, il più delle volte, si possono scrivere buone applicazioni OOP usando solo l'ereditarietà dell'interfaccia e le composizioni. I due principi
- Divieto di eredità o design per esso
- Preferisce la composizione
sono una buona guida per i motivi sopra indicati e non comportano costi sostanziali.