Il pattern Factory è un altro dei pattern più utilizzati nello sviluppo object oriented. Come Abstract Factory, analizzato in un articolo precedente, fa parte del gruppo dei pattern creazionali ed ha come scopo quello di fornire una classe capace di creare differenti implementazioni di un solo prodotto astratto.
Nella pratica risulta molto utile nelle situazioni in cui un metodo deve restituire istanze di classi diverse in base ad alcuni parametri passati. Grazie a PHP, che non ha un forte controllo sui tipi di dato, le istanze restituite dalla factory possono derivare da qualunque classe mentre la definizione del pattern richiede che tutte le classi generate da una factory implementino una medesima interfaccia.
Anche se non è necessario, è comunque importante seguire questo concetto per avere una totale congruenza con il pattern e per evitare problemi a chi utilizzerà (anche noi stessi) la nostra implementazione: se un metodo restituisce un oggetto, è bene che questo possa essere utilizzato indipendentemente dal suo comportamento specifico.
Struttura del pattern
Il pattern Factory è strutturato da quattro elementi che interoperano tra loro:
- Product: è l'interfaccia che viene implementata da tutti gli oggetti che il ConcreteCreator è in grado di generare. Usando un'interfaccia ci assicuriamo congruenza nell'utilizzo delle istanze restituite dal ConcreteCreator;
- ConcreteProduct: è l'implementazione dell'interfaccia Product che dona un comportamento specifico ai prototipi dei metodi definiti nell'interfaccia implementata. I ConcreteProduct possono essere più di uno e vengono generati dal ConcreteCreator in base a determinate condizioni (che possono essere lo stato dell'applicazione, i parametri passati al metodo che genera i prodotti, ecc ...);
- Creator: è l'interfaccia che viene implementata dai ConcreteCreator e definisce il prototipo di un metodo che si occupa di generare istanze di ConcreteProduct. Spesso l'interfaccia viene definita come una classe astratta che fornisce un'implementazione standard del metodo factory che restituisce il prodotto di default;
- ConcreteCreator: è l'implementazione dell'interfaccia Creator che sovrascrive il metodo factory per restituire istanze di ConcreteProduct specifici.
In generale si opera specificando Creator come una classe astratta e fornendo un'implementazione del metodo factory che restituisce l'istanza di uno dei ConcreteProduct definiti (che verrà stabilito come prodotto di default). Poi si definiscono varie implementazioni dell'interfaccia Product ed eventualmente varie implementazioni dell'interfaccia Creator che possono generare set di ConcreteProduct differenti. L'obiettivo è comunque quello di lasciare lasciar decidere quale istanza di ConcreteProduct creare al ConcreteCreator.
Vediamo un esempio della struttura UML utilizzata per descrivere questo pattern:
Implementazione generica
Per prima cosa vediamo un'implementazione generica del pattern, soffermandoci sulla struttura delle interfacce e delle classi definite:
<?php interface Product { public function use_product(); } class DefaultConcreteProduct implements Product { public function use_product() { echo "Ho utilizzato il prodotto DefaultConcreteProduct<br />"; } } class ConcreteProductAA implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductAA<br />"; } } class ConcreteProductAB implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductAB<br />"; } } class ConcreteProductAB implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductAC<br />"; } } class ConcreteProductBA implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductBA <br />"; } } class ConcreteProductBB implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductBB<br />"; } } class ConcreteProductBC implements Product { public function use_product() { echo "Ho utilizzato il prodotto ConcreteProductBC<br />"; } } abstract class Creator { public function factory_method($disc) { return new DefaultProduct(); } } class ConcreteCreatorA extends Creator { public function factory_method($disc) { $instance = null; switch($disc) { case 'A': $instance = new ConcreteProductAA(); break; case 'B': $instance = new ConcreteProductAB(); break; case 'C': $instance = new ConcreteProductAC(); break; default: $instance = parent::factory_method($disc); break; } return $instance; } } class ConcreteCreatorB extends Creator { public function factory_method($disc) { $instance = null; switch($disc) { case 'A': $instance = new ConcreteProductBA(); break; case 'B': $instance = new ConcreteProductBB(); break; case 'C': $instance = new ConcreteProductBC(); break; default: $instance = parent::factory_method($disc); break; } return $instance; } } function test(Creator $creator) { $products = array(); $products['A'] = $creator->factory_method('A'); $products['B'] = $creator->factory_method('B'); $products['C'] = $creator->factory_method('C'); $products['F'] = $creator->factory_method('F'); foreach($products as $key => $product) { echo "Chiave ".$key."<br />"; $product->use_product(); } } test(new ConcreteCreatorA()); test(new ConcreteCreatorB()); ?>
L'implementazione definita utilizza la nomenclatura che abbiamo seguito precedentemente ed definisce due differenti ConcreteCreator
che generano set differenti di ConcreteProduct
in base al valore del primo argomento passato a metodo factory_method
.
Il factory_method
accede, nel caso in cui il parametro passato non sia contemplato, all'implementazione esposta dalla classe astratta Creator
per generare il ConcreteProduct
di default (DefaultConcreteProduct
). Anche in questo caso possiamo notare come la funzione di test operi accedendo solamente ad i metodi prototipizzati nelle interfacce Creator e Product, e funzioni in modo corretto su qualunque implementazione fornitagli senza essere a conoscenza del comportamento specifico.
Grazie al pattern factory possiamo utilizzare i prodotti generati dal factory method senza preoccuparci minimamente dell'implementazione specifica del prodotto restituito; in questo modo delle modifiche apportate al metodo di generazione dei prodotti non sortiranno alcune malfunzionamento sul resto dell'applicazione.
Esempio di un caso reale
Prima di terminare mi piacerebbe illustrare un esempio di utilizzo del pattern in un contesto reale. L'esempio che mi appresto ad illustrare implementa un semplice sistema che genera implementazioni di un'interfaccia User con compiti e permessi differenti:
<?php abstract class User { public $name; public function __construct($name) { $this->name = $name; } public function can_write() { return false; } public function can_read() { return false; } public function can_execute() { return false; } } class DefaultUser extends User { } class Admin extends User { public function can_write() { return true; } public function can_read() { return true; } public function can_execute() { return true; } } class Reader extends User { public function can_read() { return true; } } class Writer extends User { public function can_write() { return true; } } class Executor extends User { public function can_execute() { return true; } } interface IUsersManager { public function create_user($type); } class UsersManager implements IUsersManager { private $roles; public function __construct($roles) { $this->roles = $roles; } public function create_user($name) { if(array_key_exists($name, $this->roles)) { $role = $this->roles[$name]; $class = ucfirst($role); return new $class($name); } return new DefaultUser($name); } } $um = UsersManager(array( 'gabriele' => 'admin', 'andrea' => 'reader' )); $users = array('gabriele', 'andrea'); foreach($users as $user) { $u = $um->create_user($user); if($u->can_read()) echo $u->name." può leggere <br />"; if($u->can_write()) echo $u->name." può scrivere <br />"; if($u->can_execute()) echo $u->name." può eseguire <br />"; } ?>
L'esempio è molto semplice ma fa comprendere il funzionamento del pattern in un mondo reale: in base all'utente che vogliamo recuperare (qui la ricerca viene fatta all'interno di un array, ma si potrebbe eseguire una ricerca su database per recuperare il tipo di utente) viene restituita un'implementazione differente dell'interfaccia User.
Ogni implementazione permette o meno l'esecuzione di certe operazioni. Quindi, in base all'utente, a questo sarà permesso eseguire determinate operazioni o meno. Nel caso in cui si volessero aggiungere nuove tipologie di utenti, il comportamento della parte finale dello script rimarrebbe corretto e si adatterebbe automaticamente al nuovo sistema utilizzato.