Dr. Carlo Pescio
Systematic Object Oriented Design, Parte 4

Pubblicato su Computer Programming No. 89


Riprendiamo, dopo l'ormai consueta pausa, a parlare di Systematic OOD. In questa puntata esamineremo un problema importante ma spesso trascurato, anche nelle librerie e nei framework commerciali: come gestire le dipendenze di creazione tra le classi.

Nella scorsa puntata della miniserie "Systematic OOD" abbiamo affrontato un importante argomento (la gestione delle dipendenze da classi concrete), esaminando le tecniche di trasformazione utilizzabili per riportare il design entro i canoni di estendibilità e di riusabilità tanto spesso predicati dai profeti dell'OOP, e tanto raramente ottenuti in molti casi reali.
Alcuni di voi ricorderanno l'argomento di chiusura del mio precedente articolo: se nel design originale la classe A utilizzava la classe concreta B, e nel design "trasformato" la nostra classe A fa invece riferimento ad una classe astratta B', chi si preoccuperà di creare le istanze delle inevitabili classi concrete derivate da B'?
Osserviamo infatti che se A crea le istanze delle classi (concrete) derivate da B', il problema della dipendenza da classe concreta non è stato completamente rimosso, ma solo isolato ad una fase ben specifica (la creazione).
Purtroppo, questo problema è passato inosservato per molto tempo, tanto che in buona parte delle librerie commerciali troviamo dei seri impedimenti all'estensione proprio a causa di dipendenze di creazione non gestite. In questo senso, il più grande contributo all'evidenziazione del problema è venuto proprio dal celebre [GoF94], che con i suoi pattern creazionali mostra diverse tecniche il cui fine ultimo è proprio di gestire, in modo più o meno sofisticato, una dipendenza di creazione.
Come vedremo, i pattern creazionali sono tutti ottenibili partendo da un design banale ed applicando alcune semplici tecniche di trasformazione. Vedremo anche come le tecniche presentate in questo articolo vadano oltre la generazione dei pattern presentati in [GoF94], coprendo anche altri casi e prestandosi ad un uso combinato.

Dipendenze di creazione
A beneficio di chi non ha seguito la miniserie sin dal primo numero, ricordo che il SysOOD consta di due elementi fondamentali: le regole di design (che devono specificare anche le conseguenze di una violazione) e le tecniche di trasformazione associate, che possono essere utilizzate per riportare il design entro i limiti segnati da ogni regola.
Nella presente puntata vedremo le tecniche di trasformazione associate alla terza regola del SysOOD [Pes99a], che è così formulata: se la classe A crea oggetti di classe B, la classe B deve essere final, oppure l'operazione di creazione deve essere ridefinibile (almeno nelle sottoclassi di A).
Ricordiamo che final significa che non è possibile derivare ulteriormente dalla classe. Si tratta di un concetto nativo in alcuni linguaggi (come Java), implementabile manualmente in altri (come il C++, si veda ad es. [Pes95] al capitolo 7) o semplicemente da indicare a livello di design (magari tramite uno stereotipo in UML) se non c'é modo di mapparlo su un costrutto del linguaggio target.
Le conseguenze di una violazione della regola sono piuttosto semplici: se estendiamo A, le classi derivate non potranno fare uso di versioni estese di B; se estendiamo soltanto B, la classe A non potrà utilizzare le versioni estese senza modifiche al proprio codice (aggiungendosi quindi alla lista dei casi in cui gli oggetti non portano automaticamente ad un design estendibile).
Come già discusso nel succitato [Pes99a], le conseguenze viste sopra sono talvolta accettabili. Tuttavia, ogni volta in cui gestiamo le dipendenze da classe concreta (come visto nella precedente puntata) dobbiamo seriamente considerare anche la gestione delle dipendenze di creazione, viceversa avremo un risultato utile ai fini della semplicità di manutenzione ma non della reale estendibilità del design.
Riassunta la regola di design, passiamo ora a vedere le diverse tecniche di trasformazione. Come nelle precedenti puntate, per ragioni di spazio mi limiterò a trattare le diverse tecniche in base ad un esempio astratto, rappresentato dalla figura 1 (dove la classe A crea oggetti di classe B, e con le classi "grigie" A" e B" indico il fatto che sia A che B potrebbero essere estesi per derivazione).


Factory Method
La tecnica più semplice messa a disposizione dalla programmazione ad oggetti per risolvere il problema è la seguente: togliere la responsabilità della creazione dalla classe A, e demandarla alle classi derivate (si veda la figura 2).

In pratica, ovunque nel codice della classe originale A si trovi una istruzione del tipo "new B", nella classe trasformata A' questa verrà sostituita da una chiamata a funzione virtuale, ad esempio CreateB(). Tale funzione può avere una implementazione di default nella classe A' ma, come recita la nostra regola di design, deve essere possibile ridefinirla nella classi derivate come A".
Questa semplicissima tecnica prende il nome di Factory Method, dal momento che la sua applicazione porta direttamente all'omonimo pattern discusso in [GoF94]. Pur essendo banale, può essere usata in molti casi concreti, soprattutto se stiamo scrivendo un mini-framework white box e vogliamo lasciare maggiore libertà e flessibilità a chi ne farà uso (sicuramente, sarebbe stato molto utile se in framework come MFC avessero dispensato un po' di Factory Method nei punti giusti).
Vale la pena di notare alcune importanti conseguenze di questa tecnica: la prima, e più immediatamente visibile, è che per creare oggetti di classe diversa (es. B") dobbiamo necessariamente derivare una nuova classe da A'. Vedremo tra breve che non sempre questo è l'approccio più indicato. Sicuramente è la soluzione ottimale nei casi in cui la classe derivata debba comunque esistere, ovvero nei casi in cui la classe di partenza A sia già una classe astratta.
Una ulteriore conseguenza, condivisa da gran parte (ma non da tutte) le tecniche che vedremo, è che il contenimento diretto (possibile in linguaggi come il C++) si trasforma necessariamente in contenimento tramite puntatore, con tutte le conseguenze del caso. La tecnica di Generic Creator, discussa più avanti, non soffre di questa limitazione (ma ne ha, come sempre, delle altre). Al solito, il progettista deve scegliere la tecnica di trasformazione più indicata nel contesto del suo particolare problema di design.

Un passo intermedio
Pensate ad una situazione in cui abbiamo molte classi, potenzialmente scorrelate tra loro, che si trovano nondimeno a giocare il ruolo di A nel diagramma precedente. In altre parole, pensate ad una situazione in cui abbiamo delle classi A1, A2, A3, ecc, ognuna delle quali crea oggetti di classe B, e quindi va contro la nostra regola di design. La soluzione più ovvia è forse l'applicazione ripetuta della tecnica precedente ad ognuna delle classi creatrici. Ci troveremmo però a gestire una potenziale esplosione di classi derivate, ognuna delle quali potrebbe essere stata introdotta con il solo fine di gestire la dipendenza di creazione.
Una buona tecnica di programmazione (ed anche di progettazione) è però di evitare le duplicazioni: ogni duplicazione aggiunge infatti un ulteriore costo di testing e manutenzione. Vi sono diverse tecniche per eliminare le duplicazioni del codice, le più comuni delle quali sono, nel paradigma ad oggetti, l'ereditarietà e la delega ad un'altra classe.
Capire perché l'ereditarietà non sia (in questo caso) la scelta migliore è un ottimo esercizio. Vediamo invece cosa succede quando la classe A delega la creazione ad una classe intermedia F (figura 3): la classe A ora rispetta la regola, ma F no. Apparentemente, non un grande guadagno.

Tuttavia, se pensiamo al caso in cui abbiamo N classi A1...AN, come discusso sopra, avremo una situazione decisamente più rosea: se tutte delegano alla stessa classe F la creazione degli oggetti B, abbiamo ridotto le violazioni della regola da N ad una, e possiamo ora intervenire su questa singola violazione o usando la tecnica precedente (il che, come vedremo più avanti, è un esempio di tecnica composta) o con una tecnica diversa, che sarà oggetto del prossimo paragrafo.

Indexed Creation
La nostra classe F intermedia può essere, naturalmente, intelligente a piacere. Molto spesso siamo portati a pensare alla creazione come ad una azione che non può essere resa molto più "furba" di un banale new. Proviamo però a pensare a qualche caso language-specific, o operating-system-specific, e poi a generalizzare le idee così raccolte.
In un linguaggio come Java, ad esempio, è possibile creare una istanza di una classe il cui nome sia noto solo a run-time sotto forma di stringa. Stringa che può provenire da ogni parte: dal codice, da un database o file di inizializzazione, da una selezione utente, o da una combinazione di tutto questo. È evidente che questa possibilità di creazione dinamica getta una luce diversa sul problema. Per la cronaca, anche in C++ è possibile implementare in modo elegante un supporto del tutto analogo per la creazione degli oggetti: gli abbonati alla mia newsletter "C++ Informer" possono trovare tutti i dettagli nel numero 2, nell'articolo "Un forName/newInstance in C++". Ai non abbonati, consiglio di fare un salto su www.eptacom.net/pubblicazioni.
Pensiamo ora ad un caso non troppo dissimile: in un ambiente dotato di librerie dinamiche (Unix, Windows sono due casi molto comuni) è possibile implementare una classe in una DLL, far esportare alla DLL una funzione dal nome standard (es. Create) e poi creare una istanza di un oggetto di cui non conosciamo la classe concreta, semplicemente caricando a run-time la DLL corrispondente (noto il nome, nuovamente una stringa in questo caso), prendendo l'indirizzo della funzione Create e chiamandola.
Ovviamente, in entrambi i casi il fatto che si parta da una stringa è del tutto incidentale: ad esempio, nel modello COM si parte da un numero a 128 bit, il cosiddetto GUID. Più in generale, ciò che si fa è partire da una specifica ed in qualche modo, non di rado dipendente dal linguaggio e/o dal sistema operativo, arrivare ad ottenere una istanza della classe indicata attraverso tale specifica (che può essere una stringa o qualunque altra cosa risulti vantaggiosa e/o utilizzabile nel nostro contesto).
La trasformazione diventa quindi quella di figura 4, dove sia A che F conoscono la classe Spec, ma solo F ha il compito di creare, attraverso Spec, gli oggetti di classe B (o di qualunque altra classe raggiungibile con il meccanismo utilizzato).

Notiamo che, in genere, A conoscerà poi una classe base di B, attraverso la quale potrà interagire con l'oggetto creato da F. La trasformazione Indexed Creation (così chiamata in quanto la nostra specifica è una forma di indicizzazione dei prodotti creabili) è alla base del pattern Product Trader presentato in [BR98].
L'unico vero difetto di Indexed Creation è costituito dalla sua potenziale lentezza, se confrontata con le altre tecniche qui presentate. In funzione della vita degli oggetti, questo può o meno costituire un problema.

Abstract Factory come tecnica derivata
Ritorniamo per un istante al passo intermedio rappresentato in figura 3. Cosa succede se applichiamo alle classi F e B (che violano la regola oggetto di questo articolo) la tecnica di trasformazione presentata in figura 1? Dalla classe F verrebbe tolta la responsabilità della creazione, demandata ad una classe derivata F". D'altra parte, la creazione degli oggetti B è anche l'unica responsabilità di F, che diventerebbe quindi una pura classe interfaccia. Se provate ad applicare la trasformazione, vedrete che il risultato sarà quello della figura 5: abbiamo ottenuto il pattern Abstract Factory (si veda sempre [GoF94]) partendo da un design errato ed applicando due semplici tecniche di trasformazione.

Credo sia piuttosto utile osservare che, nella descrizione di Abstract Factory, [GoF94] pone come fattore importante la creazione di diversi prodotti (classi "parallele" alla nostra B), più che la presenza di molti potenziali creatori (le classi A1...AN discusse sopra). A mio avviso è l'unica svista grave della bibbia dei pattern, perché se l'unico problema presente è quello di creare molti prodotti, in presenza di un unico creatore, è sufficiente impiegare un factory method per ogni prodotto, senza scomodare la Abstract Factory.
Quest'ultima diventa invece utilissima nella situazione che stiamo discutendo (più creatori potenziali, con uno o più prodotti, ma con l'enfasi sul numero di creatori). In questo modo possiamo infatti accentrare le responsabilità di creazione delle classi concrete in un'unica sottoclasse F", anziché dover derivare singolarmente da ogni singolo creatore potenziale.
Chi conosce bene il modello COM non farà fatica a capire come la tecnica di creazione dei componenti sia un misto di Indexed Creation (il GUID è un esempio di specifica della classe desiderata) e di Abstract Factory. Capire a fondo perché il COM tiri in ballo anche la Abstract Factory, anziché limitarsi ad una Indexed Creation, è però meno semplice di quanto sembri, e vi lascio a rifletterci sopra come esercizio.

Generic Creator
Tutte le tecniche che abbiamo visto sinora rendono la classe client A riusabile indipendentemente dalla classe concreta B", e rendono anche il nostro design estendibile, in quanto possiamo (con facilità diversa a seconda della tecnica scelta) derivare nuove classi da B' (o aggiungere nuove classi B-like nel caso dell'indexed creation) e crearne delle istanze senza dover modificare la classe A.
In cambio, abbiamo dovuto rendere il nostro design leggermente più complesso e, per ottenere l'estendibilità, abbiamo anche rinunciato ad usare direttamente gli oggetti di classe B: li useremo invece con la mediazione di un puntatore.
Tuttavia, come ho più volte osservato in passato e come ho riassunto in [Pes97a], quando siamo interessati alla sola riusabilità, senza estendibilità run-time, possiamo adottare la programmazione generica, che in questo specifico frangente ha il vantaggio di non costringerci ad interporre un livello di indirezione.
La trasformazione, rappresentata in figura 6 e denominata Generic Creator, è molto semplice: la classe originale A diventa un template A3. In esso, tutte le occorrenze di B all'interno della classe originale A si trasformano in occorrenze del parametro T.

Naturalmente il template A3 non si utilizza direttamente, ma sotto forma di istanza, come A3<B>. Se il template A3 esprime una generica azione di creazione di un oggetto di classe T, A3<B> trasformerà questa azione in una creazione di un oggetto di classe B.
Notiamo che, come nel caso dell'analoga tecnica Generic Class vista nella precedente puntata, la classe A3<B> va contro la regola che stiamo discutendo. Questo non costituisce un problema, in quanto tutto il vero codice è nel template A3, che invece rispetta la nostra regola.

Prototype
Veniamo infine all'ultima tecnica di trasformazione che ho sinora individuato. Anche in questo caso, ho dato alla tecnica il nome di uno dei pattern creazionali di [GoF94], in quanto lo genera direttamente.
Per comprendere bene l'utilità e la genesi di questa tecnica, pensate ad una situazione piuttosto comune: la classe A (ad esempio una coda di eventi) riceve un oggetto di classe (derivata da) B. Poiché desidera controllare la vita dell'oggetto, decide di farne una copia privata: di conseguenza, A ha una dipendenza di creazione da B, come da figura 1. Tuttavia la situazione è più complicata: la classe A non è neppure certa della classe concreta dell'oggetto passato: potrebbe essere una qualunque classe derivata da B, che A potrebbe anche non conoscere.
In questa situazione non possiamo utilizzare né la tecnica elementare Factory Method, né la tecnica composta di Abstract Factory. Possiamo usare l'Indexed Creation, ammesso che la classe B sia in grado di rendere nota all'esterno la propria specifica. Oppure possiamo usare la tecnica di figura 7, dove apparentemente è l'oggetto di classe B' che sa creare se stesso.

In realtà, la classe B' avrà la responsabilità (anche astratta se B' è pura interfaccia) di clonazione. Ovvero, un oggetto di classe [derivata da] B' dovrà essere in grado di clonare se stesso: notiamo che a questo punto il problema di conoscere la giusta classe concreta è scomparso, in quanto all'interno della funzione Clone() il tipo di this è noto.
In pratica, attraverso questa tecnica prendiamo un oggetto (il prototipo, da cui il nome) e gli chiediamo di crearne un altro della stessa classe. Attenzione: non necessariamente uno identico, come spesso il nome sembra suggerire: la funzione di clonazione può avere parametri come ogni altra funzione, utilizzabili per specificare un contesto di creazione (ammesso, ovviamente, che si riesca a generalizzare tale contesto a livello di B').
La tecnica richiede l'esistenza di un prototipo, e non è quindi di applicazione universale. L'utilizzo tipico prevede che il tipo dell'oggetto da creare non sia noto, tipicamente perché si è "perso" a causa del polimorfismo.

Conclusioni
Chi sta seguendo la miniserie avrà probabilmente notato come le tecniche di trasformazione del SysOOD siano spesso poste in relazione ad alcuni design pattern. Questo non è casuale: sia il SysOOD che i pattern cercano nella pratica le soluzioni a problemi di design ricorrenti. Rimane invece piuttosto diversa la prospettiva, e come avete visto in più di una occasione le tecniche del SysOOD finiscono per costituire, tra l'altro, i "building block" dei pattern. Capire bene le tecniche di trasformazione è quindi anche un ottimo modo per capire meglio i pattern, per ricordarli meglio (o per ricostruirli se non li ricordiamo esattamente), e per evitare di perdersi nella confusione di pattern elementari, composti, variati, derivati, ecc, che peraltro stanno influenzando in modo negativo [AC98 ] uno degli aspetti più interessanti dei pattern (l'uso come "dizionario allargato" di design).
Concludo con il consueto invito ai lettori a non esitare a farsi sentire via email con suggerimenti, commenti, idee e riflessioni su quanto hanno appena letto e su quanto vorrebbero vedere trattato in futuro. Nonché con un invito a provare a mettere in pratica, nel lavoro di ogni giorno, quanto stiamo discutendo nel corso dei mesi: vedrete che si tratta di un approccio semplice e molto concreto, che si sposa facilmente con qualunque metodologia e tool utilizziate per la progettazione, e con buona parte dei linguaggi più usati per l'implementazione.

Bibliografia
[BR98] Dirk Bäumer, Dirk Riehle, "Product Trader", in Pattern Languages of Program Design Vol. 3, Addison-Wesley, 1998.
[GoF94] Gamma ed altri, "Design Patterns", Addison-Wesley, 1994.
[Pes95] Carlo Pescio, "C++ Manuale di Stile", Edizioni Infomedia, 1995.
[Pes97a] Carlo Pescio, " Programmazione ad Oggetti e Programmazione Generica ", Computer Programming No. 62.
[Pes99a] Carlo Pescio, "Systematic Object Oriented Design, Parte 1", Computer Programming No. 81.
[AC98] Ellis Agerbo, Aino Cornils, "How to Preserve the benefits of Design Patterns". Proceedings of ACM OOPSLA '98.

Biografia
Biografia Carlo Pescio svolge attivitā di consulenza e formazione in ambito internazionale nel campo delle tecnologie Object Oriented. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. Č l'ideatore del metodo di design ad oggetti SysOOD (Systematic Object Oriented Design), ed autore di numerosi articoli su temi di ingegneria del software, programmazione C++ e tecnologia ad oggetti, apparsi sui principali periodici statunitensi. Laureato in Scienze dell'Informazione, č membro dell'ACM e dell'IEEE.