Con questo articolo apro una nuova tipologia di articoli che si occuperanno di trattare teoricamente e praticamente i design pattern in PHP. Ho deciso di iniziare con l'Abstract Factory dato che è un pattern molto utilizzato nei framework e risulta molto comodo per risolvere un gran numero di situazioni controverse o diversamente gestibili.
Un design pattern (temine che proviene dall'ingegneria del software) può essere definito come una soluzione progettuale generica ad un problema che si presenta in modo ricorrente. Esso non è una libreria specifica o un componente software riusabile, bensì una descrizione o un modello da applicare per risolvere un problema che può presentarsi in diverse situazioni durante la progettazione o lo sviluppo di software differenti, con linguaggi ed ambiti differenti. Rispetto a un algoritmo il design patter risolve problemi legati agli aspetti progettuali, mentre il primo risolve problemi computazionali.
Il pattern Abstract Factory fa parte del gruppo dei pattern creazionali ed ha come scopo quello di fornire un'interfaccia per creare famiglie di oggetti connessi o dipendenti tra loro in modo che al client non venga richiesto di specificare esplicitamente il nome delle classi concrete all'interno del proprio codice. In questo modo si facilita la creazione di un sistema indipendente dall'implementazione degli oggetti concreti che lascia al client la possibilità di utilizzare diverse famiglie di oggetti correlati.
Struttura del pattern
Il pattern è strutturato da cinque elementi che interoperano tra loro:
- AbstractFactory: un'interfaccia che dichiara le operazioni che possono essere richiamate per generare gli AbstractProduct;
- ConcreteFactory: implementa le operazioni esposte dall'interfaccia AbstractFactory al fine di generare dei ConcreteProduct;
- AbstractProduct: un'interfaccia che dichiara la struttura di un oggetto che può essere prodotto dalla factory;
- ConcreteProduct: implementa l'interfaccia AbstractProduct al fine di definire uno dei prodotti concreti che possono essere generati dalle factory;
- Client: utilizza le interfacce AbstractFactory e AbstractProduct per gestire l'intero sistema;
In generale si opera creando una sola istanza di ConcreteFactory che viene utilizzata per gestire la creazione di una specifica famiglia di oggetti (ConcreteObject). Altre factory possono essere create per gestire oggetti concreti di un'altra famiglia. È buona norma definire le ConcreteFactory
come Singleton (un altro pattern che discuteremo in un'altra sede che viene utilizzato per avere una sola istanza di un oggetto condivisa tra gli ambienti che la utilizzano) per assicurarsi che ne venga creata una sola per tipo.
Vediamo un esempio della struttura UML utilizzata per descrivere questo pattern:
![La struttura UML del pattern Abstract Factory](https://www.html.it/articoli/1923/abstract.gif)
Implementazione generica
Per prima cosa vediamo un'implementazione generica del pattern, soffermandoci sulla struttura delle interfacce e delle classi definite:
<?php interface AbstractFactory { public function CreateProductA(); public function CreateProductB(); } class ConcreteFactory1 implements AbstractFactory { public function CreateProductA() { return new ProductA_1(); } public function CreateProductB() { return new ProductB_1(); } } class ConcreteFactory2 implements AbstractFactory { public function CreateProductA() { return new ProductA_2(); } public function CreateProductB() { return new ProductB_2(); } } interface AbstractProductA {} interface AbstractProducB { public function interact(AbstractProductA $a); } class ProductA_1 implements AbstractProductA {} class ProductB_1 implements AbstractProductB { public function interact(AbstractProductA $a) { printf("Eseguito il metodo ProductB_1:: interact tra %s e %s", get_class($this), get_class($a)); } } class ProductA_2 implements AbstractProductA {} class ProductB_2 implements AbstractProductB { public function interact(AbstractProductA $a) { printf("Eseguito il metodo ProductB_2::interact tra %s e %s", get_class($this), get_class($a)); } } class Client { private $abstract_product_a; private $abstract_product_b; public function __construct(AbstractFactory $factory) { $this->abstract_product_b = $factory->CreateProductB(); $this->abstract_product_a = $factory->CreateProductA(); } public function test() { $this->abstract_product_b->interact($this->abstract_product_a); } } function test() { $factory_1 = new ConcreteFactory1(); $client_1 = new Client($factory_1); $client_1->test(); $factory_2 = new ConcreteFactory2(); $client_2 = new Client($factory_2); $client_2->test(); } test(); ?>
L'implementazione definita utilizza la nomenclatura che abbiamo seguito precedentemente ed definisce due differenti factory che operano su implementazioni differenti dello stesso tipo di prodotto. La struttura delle interfacce è molto semplice e l'unico metodo implementato è interact
che permette l'interazione tra un oggetto AbstractProductA con un oggetto AbstractProductB. Possiamo notare che, indipendentemente dall'implementazione di queste due interfacce, il comportamento del client è sempre corretto ed omogeneo.
Le conseguenze dell'utilizzo di questo pattern sono lampanti: i client utilizzano le factory ed i prodotti solamente attraverso le interfacce, assicurandosi che i dati in arrivo siano sempre conformi ad una serie di regole previste in precedenza. In questo modo possono operare sui valori restituiti dai metodi di creazione senza doversi preoccupare di eventuali incongruenze nell'implementazione che, grazie all'incapsulamento, non potranno verificarsi.
Oltretutto è possibile cambiare la famiglia di prodotti utilizzata senza dover agire sul codice del client: creando una nuova ConcreteFactory e passandola come argomento al client. L'unica pecca di questo approccio è il fatto che, nel caso volessimo aggiungere nuovi prodotti, saremmo obbligati a modificare tutte le interfacce e tutte le implementazioni di queste ultime per adattarli alle modifiche.
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 una semplice struttura in grado di renderizzare la rappresentazione grafica di alcuni prodotti in base al sistema di output scelto:
<?php interface AbstractRenderer { public function createTitle(); } interface AbstractTitle { public function set_content($content); public function render(); } class HtmlTitle implements AbstractTitle { private $title; public function set_content($content) { $this->title = $content; } public function render() { return "<h1>".$this->title."</h1>"; } } class WikiTitle implements AbstractTitle { private $title; public function set_content($content) { $this->title = $content; } public function render() { return "= ".$this->title." ="; } } class HtmlRenderer implements AbstractRenderer { public function createTitle() { return new HtmlTitle(); } } class WikiRenderer implements AbstractRenderer { public function createTitle() { return new WikiTitle(); } } function test(AbstractFactory $factory) { $title = $factory->createTitle(); $title->set_content('Prova'); echo $title->render(); } $wiki_factory = new WikiFactory(); $html_factory = new HtmlFactory(); test($wiki_factory); test($html_factory); ?>
Nello script precedente ho creato un semplice esempio di client (test) che opera sulle factory per generare un titolo di un'ipotetica pagina. Come possiamo vedere l'implementazione del client non varia minimamente anche se si utilizza una factory diversa, dato che entrambe le factory si adeguano al pattern Abstract Factory e per questo motivo espongono una base di funzionalità identiche. Ovviamente l'esempio può essere esteso aggiungendo nuove factory o creando nuove tipologie di prodotti.
Se PHP fosse stato un linguaggio per lo sviluppo desktop (o se avesse avuto più di un alternative a PHP-GTK per questa tipologia di sviluppo) avremmo potuto strutturare lo script in modo che producesse widget specifici di una libreria in base ad un semplice parametro di configurazione, che ci avrebbe fatto variare solamente la factory scelta inizialmente.
Conclusioni
Il pattern Abstract Factory ha una quantità di utilizzi veramente impressionante ed una volta acquisita una certa dimestichezza con il suo utilizzo lo si può utilizzare in un gran numero di situazioni spontaneamente. Ricordo che è sempre buona norma definire le ConcreteFactory
come singleton al fine di condividere un'unica istanza per ogni ConcreteFactory
istanziata. Un'altra tecnica implementativa interessante consiste nell'utilizzare il pattern Factory per creare le implementazioni degli AbstractProducts
oppure il pattern Prototype al fine di definire dei prototipi contenenti le operazioni simili svolte dalle diverse implementazioni dei ConcreteProduct
.
Come vedete un pattern tira l'altro. La prossima volta tratteremo un altro pattern interessante, cercando di studiare come possa cooperare con quello implementato oggi.