La
creazione di oggetti e dati membro all'interno di un classe può
essere un problema per le classi derivate: vediamo come questo pattern
semplifica e risolve le cose, come sempre in modo piuttosto elegante e
riutilizzabile. Vedremo in particolare i pattern factory method ed abstract
factory.
Introduzione
al problema
I dati membro
di una classe, come ben si sa, possono avere come tipo, a loro volta una
classe, o, come si suol dire, possono essere di tipo strutturato, e non
primitivo. In questi casi, spesso si legge sui libri di programmazione,
che sarebbe meglio memorizzare dei puntatori, invece di oggetti veri e
propri; si possono dare due motivazioni a questo consiglio (per altro molto
giusto):
-
memorizzando direttamente
un oggetto, la creazione di tale oggetto avverrà insieme alla creazione
dell'oggetto che lo contiene; con i puntatori si può ritardare tale
creazione al momento opportuno, e si può anche evitare di creare
un oggetto se questo non servirà, risparmiando così spazio
e tempo (necessario per la creazione); questo è un consiglio che
vale anche nella programmazione strutturata tradizionale, mentre la prossima
motivazione è dettata dalla filosofia della programmazione ad oggetti:
-
in questo modo si
può sfruttare il polimorfismo, grazie all'ormai famosa proprietà
che:
un puntatore
ad un oggetto di classe A può puntare anche ad un qualsiasi oggetto
di classe derivata da A
Questo problema
non si pone in Java, in quanto dichiarando un dato membro di un tipo T
non si crea un oggetto di tipo T (cioè viene semplicemente
dichiarato un riferimento: si dovrà esplicitamente istanziare un
tale oggetto con new T). Quindi in Java, implicitamente, i dati
membro di tipo strutturato sono tutti puntatori.
Effettivamente
quando si deriva da una classe si ereditano tutti i suoi campi: in base
al fatto che siano dichiarati come protected o private si
potrà o non si potrà accedere a tali campi in modo diretto
nella classe derivata, ma sarà sempre possibile accedervi tramite
metodi; si tenga però presente che, nel caso siano stati dichiarati
privati, si presuppone che il creatore della classe base non voglia che
si possa accedere direttamente a tali dati, ma che si utilizzino semplicemente
le operazioni che si possono effettuare su di essi (come delle black
box, di cui non sono noti i dettagli interni). Alla base di questo
tipo di ragionamento sta l'information hiding, che non vuole essere
solo un mezzo per nascondere il proprio lavoro agli altri, quanto un mezzo
per permettere una maggiore riusabilità: facendo assunzioni sulla
struttura di certi campi di una classe, ed usandoli direttamente, si renderà
quel codice esposto a possibili errori futuri, nel caso vengano modificati
dei dettagli interni di tali campi; al contrario l'accesso tramite interfacce,
permette di non preoccuparsi di future modifiche.
Del resto questo
può sembrare limitativo nel caso si crei una gerarchia di classi
ben progettata che sfrutta al massimo il polimorfismo: se si deriva da
una classe A si può voler specializzare il comportamento
di tale classe in particolari situazioni; per far questo però si
può dover specializzare anche il comportamento di certi membri.
Un esempio
di problema
Consideriamo
un esempio (tratto da [GOF95], e molto comune in diversi framework, come
le MFC):
In un framework
per applicazioni si possono avere più tipi di documenti (grafici,
testuali, ecc...). Le astrazioni fondamentali per questo tipo di framework
sono l'applicazione ed il documento. Ovviamente di tratta
di classi astratte, che il programmatore dovrà utilizzare come classi
base, per scrivere le proprie applicazioni (derivando da applicazione)
che faranno uso di particolari documenti (anche in questo caso derivando
dalla classe astratta documento).
La creazione
dei documenti è un'operazione che deve essere svolta dal framework,
tuttavia, per far questo, dovrebbe essere in grado di conoscere in anticipo
il tipo di oggetto (di classe derivata) da creare; per altro la classe
documento
sarà astratta e quindi non potrà essere istanziata.
Oppure si può
pensare ad una classe graf che rappresenta un oggetto grafico che
contiene come membro un oggetto di classe visualizzatore, che sarà
utilizzato per visualizzare l'oggeto grafico sullo schermo; derivando dalla
classe dell'oggetto grafico si può anche volere modificare la modalità
di visualizzazione, ed in tal caso si specializzerà anche la classe
visualizzatore, con una classe derivata. Nella classe derivata da graf
si dovrà creare un oggetto di classe derivata da visualizzatore
da assegnare al campo membro (notare che l'assegnamento è possibile
perché si sono utilizzati puntatori). Supponiamo che la nostra classe
base graf sia così strutturata:
public class
graf {
protected visualizzatore vis ;
...
public graf() {
vis = new visualizzatore() ;
...
}
...
public visualizza() { vis.visualizza() ; }
} |
siamo giunti
al nostro problema: nella classe derivata come fare ad istanziare un visualizzatore
personalizzato (la cui classe deriva da visualizzatore)?
Tale oggetto
viene istanziato nel costruttore, e quindi non si ha la possibilità
di crearne uno differente; una soluzione banale è quella di ricreare
l'oggetto, ma questo oltre ad essere poco elegante (ed a questo qualcuno
potrebbe anche non essere interessato), rischia di creare malfunzionamenti
nella classe base soprattutto se si devono passare dei parametri al costruttore
del visualizzatore. La cosa è ancora più complessa se non
si può accedere ai parametri da passare, cioè se li può
passare solo la classe base!
La soluzione
in questi casi è molto semplice: il pattern factory method
(factory = fabbrica): nella classe base, invece di creare il visualizzatore
esplicitamente, lo si crea chiamando un metodo; nella classe derivata basterà
ridefinire tale metodo (eventualmente mantenendo la stessa signature, nel
caso gli si debbano passare dei parametri). In questo modo nella classe
derivata forniamo il metodo per creare un visualizzatore (e quindi anche
uno appartenente ad una classe derivata), e sarà la classe base
che al momento opportuno lo chiamerà. Vediamo nei seguenti pseudo
listati l'implementazione di questa idea.
La classe base
diventerà:
public class
graf {
protected visualizzatore vis ;
...
public graf() {
vis = nuovoVisualizzatore() ;
...
}
protected visualizzatore nuovoVisualizzatore() {
return new visualizzatore() ;
}
...
public visualizza() { vis.visualizza() ; }
}
|
dove viene definito
il factory method nuovoVisualizzatore.
mentre la classe
derivata MIOgraf, che utilizza un visualizzatore specializzato MIOvisualizzatore
(derivato ovviamente da visualizzatore), sarà:
public class
MIOgraf {
public MIOgraf() {
...
}
protected visualizzatore nuovoVisualizzatore() {
return new MIOvisualizzatore() ;
}
...
} |
dove è
stato ridefinito il factory method suddetto (si noti che nel costruttore
non lo si invoca: lo invocherà la classe base). Si noti inoltre
che non è affatto necessario ridefinire il metodo visualizza
della classe base: questo si basa sul metodo omonimo del visualizzatore
e grazie al polimorfismo sarà richiamata la versione giusta di tale
metodo.
In un certo
senso la filosofia che sta dietro al pattern factory method si può
riassumere informalmente in questa situazione: è come se la classe
base dicesse
OK: io utilizzerò
un visualizzatore; le classi derivate potranno utilizzare un visualizzatore
personalizzato, ed io darò loro l'opportunità di creare questo
visualizzatore personalizzato, purché loro mi mettano a disposizione
un metodo per fare ciò.
...più
formalmente
Vediamo in modo
più formale i partecipanti a questo pattern:
-
prodotto:
definisce l'interfaccia degli oggetti che crea il factory method
(il visualizzatore del nostro esempio)
-
creatore:
definisce il factory method che restituisce un oggetto di tipo prodotto.
Abstract Factory
Una generalizzazione
del precedente pattern è il pattern Abstract Factory: in
questo caso si ha una classe il cui unico scopo è quello di creare
degli oggetti prodotto, ed è quindi costituita solo (o almeno per
la maggior parte) di factory method.
Questo torna
comodo quando si devono creare diversi oggetti prodotto collegati logicamente
fra loro: in questo modo, invece di cospargere le varie classi che ne fanno
uso di tanti factory method, si racchiudono tutti questi metodi in una
classe astratta, e poi si specializza direttamente tale classe; i vari
client
dei prodotti utilizzeranno una particolare abstract factory per
creare i propri oggetti.
Vi lascio adesso
ad Andrea Trentini, che vi mostrerà qualche esempio concreto di
implementazione di quanto visto finora.
Bibliografia
-
[GOF95] E.Gamma,
R.Helm, R.Johnson, J.Vlissides, Design Patterns, Elements of Reusable
Object-Oriented Software, 1995, Addison-Wesley.
|