Composizione e delega¶
Questa breve nota presenta la composizione e la delega come possibili alternative all'ereditarietà.
La nota segue l'esposizione del Capitolo 4 (e in particolare dell'Item 18) del testo "Effective Java" (di Joshua Bloch).
Estendere o utilizzare?¶
Supponiamo di avere una classe Adder
in grado di tenere traccia di somme tra interi (che possono essere svolte considerando un numero per volta oppure prelevando gli addendi da una lista), il cui codice è
Supponiamo ora di voler costruire una nuova classe in grado di tener traccia non solo della somma, ma anche dei valori di volta in volta sommati.
Dato che tale funzionalità sembra estendere quella della classe appena introdotta, sembra naturale procede per estensione, definendo la classe LogAdderExt
come segue
Si osservi che (basandosi sulle specifiche del supertipo) appare assolutamente corretta l'idea di sovrascrivere entrambi i metodi add
, anche quello evidenziato in giallo.
L'esecuzione di un semplice client di tali implementazioni
mostra però un problema, poiché produce l'output
Seppure le somme sono corrette in entrambi i casi, la lista dei valori aggiunti è evidentemente duplicata. Cosa è successo?
Per capirlo possiamo eseguire di nuovo il codice del client dopo aver instrumentato il codice dei metodi add
delle classi Adder
e LogAdderExt
con cali, uno strumento che permette di aggiungere automaticamente codice ai metodi per tracciarne le invocazioni.
Quel che accade è che l'implementazione del metodo add
per la lista di LogAdderExt
, dopo aver aggiunto alla lista gli interi passati come argomento, invoca il metodo del supertipo il quale, a sua volta, invoca il metodo add
per il singolo intero; questo, a causa del fatto che il tipo concreto è LogAdderExt
, risulta in una chiamata del suo metodo add
per un singolo intero (che aggiunge di nuovo il numero alla lista) che infine chiama il metodo del supertipo che svolge la somma.
Osservate è molto improbabile basarsi sulle specifiche per evitare un problema del genere, dato che non è necessario che una specifica segnali che adopererà altri metodi della classe per svolgere il suo compito — a meno che la classe non sia esplicitamente specificata (e documentata) per essere estesa.
Composizione¶
Una soluzione semplice a questo problema è fare in modo che la classe che somma e tiene traccia dei numeri non estenda Adder
ma lo adoperi, attraverso la composizione, come nella classe LogAdderComp
In questo caso, dato che adder
e seen
sono oggetti distinti, non è possibile alcuna commistione tra le chiamate dei loro metodi e, come mostrato dal client seguente, l'errore di prima non si presenta
Si può fare meglio¶
La soluzione precendente presenta però un limite: i due tipi non sono in alcuna relazione, per cui non è possibile sostiuire facilmente uno all'altro. L'ideale sarebbe avere una interfaccia AdderInterface
come la seguente
che consenta di scrivere codice come
consentendo di decidere in un secondo momento se usare un semplice Adder
(il cui codice è identico a quello presentato in precedenza, fatta eccezione per il fatto che nell'intestazione della classe aggiungeremo implements AdderInterface
) che tenga traccia della somma, o una variante di esso in grado di tener traccia anche degli addendi.
Ma non vogliamo incorrere nell'errore precedente.
Per questa ragione astraiamo l'idea di usare il supertipo in una classe che implementi l'interfaccia di cui sopra, delegando (forward) ogni metodo al supertipo. Questa classe si comporta un po' come LogAdderComp
, ma non altera alcun comportamento; si occupa solo di "girare" al supertipo tutte le chiamate.
Utilizzando tale "guscio" è molto semplice ottenere il risultato cercato:
- ottenendo un sottotipo della medesima interfaccia che soddisfa
Adder
, - non incorrendo nel rischio di un rimbalzo tra i metodi del supertipo e del sottotipo.
Osservate che, tra l'altro, in questa classe è sufficiente sovrascrivere solo i metodi add
perché a implementare le funzionalità del metodo result
ha già provvedduto il forward contenuto nella classe AdderForwarder
; ovviamente va aggiunto il metodo log
relativo alla nuova funzionalità provvista dal sottotipo.
Ora è del tutto banale poter invocare use
sul sommatore, o sul sommatore che tenga traccia degli addendi.
Osservate che c'è un vantaggio in più: LogAdder
può scegliere nel suo costruttore a che implementazione di AdderInterface
fare riferimento; se in futuro ci fossero implementazioni migliori, LogAdder
, non essendo vincolato a essere un sottotipo di Adder
, potrebbe essere utilizzato con una qualunque di tali implementazioni.
L'esecuzione del codice client mostra il comportamento desiderato