La programmazione ad oggetti è innanzitutto modularità. Ci hanno insegnato sempre così ed è così che ci piace. L’approccio “scrivi una volta usa per sempre”, con le dovute precisazioni, è ciò che divide i pigri dagli “inutilmente operosi” ed è questo che ci rende fieri di essere informatici.
Tuttavia durante la pratica dell’arcana scienza dello programmare codice ci troviamo spesso in situazioni in cui le nostre certezze vacillano. Ci troviamo dinanzi a problemi in cui l’unica soluzione sembra l’innesto di codice specifico in classi che nella nostra mente appaiono bellissimi esempi di indipendenza, astrazione e modularità.
Per esempio immaginiamo di trovarci ad affrontare il problema di caricare delle risorse da un file XML (che è proprio il caso che ho affrontato io, ne parleremo poi). Queste risorse possono essere dei tipi più variegati: immagini, audio, file di testo e chi più ne ha più ne metta. Istintivamente il nostro codice sarà del tipo:
switch (resource_type) :
case "image" :
..crea risorsa di tipo immagine..
case "audio" :
..crea risorsa di tipo audio..
case "testo" :
..crea risorsa di tipo testo..
A questo punto notiamo subito un dettaglio preoccupante: dobbiamo includere nella nostra classe i riferimenti a tutte le classi di interesse. Questo significa che una classe che deve limitarsi a caricare risorse da un file deve anche conoscere come queste risorse vengono costruite. Eppure sono tutte “risorse”, un unico grande e astratto tipo di dati. Perché devo conoscere anche le implementazioni particolari?
A questo punto per togliere questi riferimenti dal codice di solito si pensa all’uso dei template. Ma a me i template non piacciono e cerco di servirmene il meno possibile. Infatti i template comportano sempre un numero imprecisato di problemi, vengono usati a tempo di compilazione per generare il “codice giusto” (C++) oppure rimangono sempre incoerenti con il resto del linguaggio (Java). Insomma, ogni volta che scrivo template
nel codice è come se mi beccassi un pugno. Il C++11 risolve molti di questi problemi ma è ancora troppo al confine per essere utilizzato.
Inoltre in questo caso anche la logica ripudia l’uso dei template: essi infatti vanno utilizzati per definire classi o funzioni in grado operare su un tipo generico di dati. Ad esempio una lista può contenere qualsiasi oggetto, sia esso un numero o una stringa. Non esiste nessuna dipendenza o parentela fra stringhe e numeri. I numeri non sono stringhe e le stringhe non sono numeri. Nel nostro caso invece la dipendenza c’è: tutte le risorse particolari sono una sotto-classe di una generica classe astratta Risorsa. È sbagliato utilizzare un template per gestire cose che sono di dominio del polimorfismo! Se una classe deve gestire istanze particolari di una classe astratta (o interfaccia) allora bisogna usare il polimorfismo.
Ma allora come possiamo fare? L’idea più semplice è affidarsi ad una Factory Class. Una Factory Class è una classe che ha il solo scopo di generare istanze concrete di un interfaccia oppure sotto-classi specifiche di una classe astratta (che è più o meno la stessa cosa). La classe cliente, nel nostro caso la funzione che carica risorse da un file, non dovrà più conoscere l’implementazione di tutte le sotto-classi ma si limiterà a contattare il factory.
Lo switch precedente si trasforma in una semplice riga di codice perché tutto il lavoro è passato alla Factory. Da adesso in poi il nostro caricatore di risorse potrà fregarsene, come è giusto che sia, di quale specifica risorsa sta caricando: lui carica risorse, il resto sono dettagli inutili.
Vediamo quindi come è implementata la Factory. Di solito qui si commette un errore molto comune: implementare il metodo CreateResource(type)
in questo modo.
switch (resource_type) :
case "image" :
..crea risorsa di tipo immagine..
case "audio" :
..crea risorsa di tipo audio..
case "testo" :
..crea risorsa di tipo testo..
}
Avete 10 secondi per trovare l’errore. Fatto? Bene. Abbiamo semplicemente spostato lo switch in ResourceFactory
! Abbiamo spostato il problema in ResourceFactory
ma non l’abbiamo risolto!
Supponete di voler rilasciare il vostro ResourcesManager
come libreria. Poiché c’è una dipendenza stretta fra il ResourcesManager
e il ResourcesFactory
dovete ovviamente includerli entrambi nella libreria. Ma poiché il Factory dipende da tutti i suoi prodotti siete costretti ad includere nella libreria tutti i prodotti! Allarme! Voi non sapete l’utente finale quali risorse deve gestire: non sapete se i limiterà alle risorse che avevate pensato voi oppure deciderà di usarlo per mozzarelle e pomodori!
A questo punto potete decidere di fregarvene, mettere un buon corredo di risorse e tirare avanti ma così non state rilasciando un ResourcesManager
bensì un ImageAudioTextEtcManager
.
Per vostra fortuna la soluzione elegante esiste ed è più semplice di quello che si pensa.
public:
typedef Resource* (*ResourceCreator)();
void registerProduct(const std::string &type,
ResourcesFactory::ResourceCreator producer);
Resource* createResource(const std::string &type);
private:
std::map<std::string,ResourceCreator> _factory_map;
Questa è la base. Semplicissima! Bastano due metodi ed un dizionario. Vediamola in dettaglio.
Innanzitutto la classe ResourcesFactory
deve essere una classe Singleton e non una classe statica come il precedente esempio noob. La necessita deriva dal fatto di dover interagire sempre con il dizionario _factory_map
. Inoltre deve essere possibile accedere a questa classe da ogni classe specializzata per effettuare la registrazione.
Il metodo chiave di quest’implementazione è registerProduct(const std::string &type, ResourcesFactory::ResourceCreator producer
. Questa funzione prende due parametri:
- Una stringa (ma può essere anche un numero o qualunque altro identificatore) che rappresenta il tipo specifico che si vuole creare.
- Un puntatore alla funzione che genera la specializzazione indicata dalla stringa precedente. La funzione puntata deve restituire un puntatore alla classe astratta superiore, in questo caso Resource.
Ad esempio supponiamo di avere una risorsa di tipo immagine descritta dalla classe ImageRes
che ovviamente eredita da Resource
. Per registrare questa classe alla factory basterà dare il comando
Questa funzione non farà altro che aggiungere la coppia tipo-costruttore al suo dizionario. La funzione BuildImageRes
è ovviamente una funzione statica definita in ImageRes
che non fa altro che costruire una sua istanza e restituirla (una specie di “meta-costruttore” per intenderci). Da questo momento in poi la nostra factory sarà in grado di costruire oggetti di tipo ImageRes
perché sa quale funzione chiamare quando gli viene chiesto di costruire un “image”.
A questo punto la funzione createResource
è piuttosto banale. Vi basterà cercare nel dizionario una funzione “costruttrice” associata al tipo richiesto ed invocarla restituendone il risultato.
La potenza di questo metodo credo vi sia ormai chiara: avete completamente disaccoppiato creazione e caricamento di risorse delle singole specializzazioni. Ora potete distribuire la tripla ResourcesManager
,ResourcesFactory
, Resource
senza curarvi di decidere a priori con quali risorse verranno utilizzate.
Se il vostro cliente vuole usare il vostro codice per gestire mozzarelle gli basterà:
- Scrivere una classe
MozzarellaResource
che eredita (o implementa, se il vostro linguaggio supporta le interfacce) daResource
. - Scrivere una funzione statica
mozzarellaProducer
che crea se stessa e restituisce il risultato sotto forma di un puntatore aResource
. - Registrare la classe al factory tramite il metodo
registerProduct("mozzarella", MozzarellaResource::mozzarellaProducer)
Fine. Nulla di più. Il vostro gestore di risorse è ora usufruibile da tutti anche per risorse che vanno oltre la vostra più ardita immaginazione. Ovviamente quello che abbiamo scritto adesso per la gestione delle risorse può essere utilizzato pari pari per qualunque altro vostro caso d’uso.
Come se non bastasse è inoltre possibile far si che la nuova risorsa si registri alla factory in modo automatico (o semi-automatico) in fase di definizione. Per non mettere troppa carne al fuoco parleremo di questo in un’altra occasione. Ringrazio comunque Fulvio Esposito per la consulenza concessami. 🙂
bello, mi ha risparmiato un po’ di grattacapi iniziali nel capire queste “factory” che trovavo in giro 😛