Cos’è davvero il Factory Pattern

Un’architettura ben progettata, che sfrutta il Factory Pattern, permette di isolare la logica di istanziazione da chi usa l’oggetto, ridurre il rischio di introdurre bug durante un refactoring e preparare il codice a future estensioni, evitando l’overengineering prematuro.
Questo articolo approfondisce le ragioni per cui il pattern è fondamentale, illustra un esempio pratico con diverse implementazioni in PHP, Java, Python e C++, e fornisce indicazioni su quando è davvero opportuno adottarlo.
Il problema reale senza Factory: new ovunque e logica sparsa
Senza una factory, la creazione degli oggetti finisce in ogni angolo dell’applicazione: controller, service, job asincroni, listener di eventi. Ogni componente decide autonomamente quale classe concreta istanziare. Questo approccio disperde la decisione di scelta dell’implementazione, rendendo difficile qualsiasi modifica successiva.
Se domani desideri sostituire una classe concreta con un’alternativa, sei costretto a riscrivere tutti i punti in cui era stato usato new. Il refactoring diventa non solo noioso, ma anche pericoloso: ogni modifica è un potenziale punto d’ingresso per bug, soprattutto in progetti di grandi dimensioni o con team distribuiti.
Framework moderni come Laravel, Spring o Angular semplificano l’injection automatica, ma non eliminano la necessità di una gestione centralizzata della logica di creazione. Ignorare questo aspetto porta a un accoppiamento stretto tra le classi concrete e il resto del sistema, limitando la capacità di evoluzione e manutenzione del codice.
L’esempio classico: Car, ElectricCar e DieselCar
Immagina di avere un’interfaccia o classe base Car e diverse implementazioni concrete: ElectricCar, DieselCar e, in scenari più complessi, HybridCar, SimulatedCar o versioni che si integrano con API esterne. La decisione su quale tipo di auto creare non dovrebbe avvenire nei punti in cui l’auto viene utilizzata, ma all’interno di una factory dedicata.
Questo modello consente al chiamante di lavorare esclusivamente con il contratto Car, senza conoscere le specifiche implementazioni. In caso di aggiunta di nuove tipologie, basta aggiornare la factory, mantenendo intatto il resto del codice. La flessibilità ottenuta è fondamentale per progetti che devono adattarsi rapidamente a nuove esigenze di business.
L’obiettivo dell’esempio non è mostrare un veicolo reale, ma evidenziare come la separazione delle responsabilità tra creazione e utilizzo migliori la manutenibilità e riduca il rischio di rotture a catena quando il dominio evolve.
Implementazioni pratiche
Implementazione in PHP
In PHP, la factory può essere una classe con un metodo statico che restituisce un’istanza in base al tipo richiesto. Questo approccio si integra perfettamente con i container di dipendenze tipici dei framework moderni, come Symfony o Laravel, permettendo di configurare le scelte di implementazione per ambiente.
La centralizzazione della creazione semplifica test unitari, poiché la factory può essere facilmente mockata o sostituita con versioni di test. Inoltre, aggiungere una nuova classe HybridCar richiede un solo cambiamento nella factory, evitando duplicazioni di codice nei vari service.
Il vantaggio è immediato: il resto dell’applicazione continua a parlare con l’interfaccia Car senza conoscere i dettagli delle classi concrete.
Questa struttura rende il codice scalabile e pronto a gestire configurazioni diverse per sviluppo, test e produzione, mantenendo il principio di dipendenza dall’interfaccia anziché dall’implementazione concreta.
Implementazione in Java
In Java, il pattern è quasi indispensabile quando si combinano interfacce, iniezione delle dipendenze (Spring) e configurazioni XML o annotation. La factory nasconde la logica di scelta dell’implementazione, lasciando il resto dell’applicazione indipendente dal tipo concreto di Car.
L’uso di exception handling specifiche, come IllegalArgumentException, rende più chiara la segnalazione di errori di configurazione. Inoltre, la factory può essere registrata come bean singleton in Spring, permettendo di sfruttare la gestione del ciclo di vita offerta dal framework.
Anche in scenari complessi, dove le classi concrete vengono caricate dinamicamente da plugin o moduli esterni, la factory rimane il punto di controllo centrale.
Con questa architettura, le modifiche future – ad esempio l’introduzione di un HybridCar – richiedono solo un aggiornamento della factory, mantenendo intatto il codice di business.
Implementazione in Python
Python, con la sua natura dinamica, può sembrare meno incline a pattern strutturati. Tuttavia, quando il progetto cresce, la coerenza nella creazione degli oggetti diventa cruciale per evitare confusione e dipendenze nascoste.
La factory in Python è spesso implementata come metodo statico o come semplice funzione, ma l’importante è che la logica di scelta rimanga centralizzata. Questo facilita il testing automatizzato, poiché è possibile iniettare versioni mock o stub della factory nei test di unità.
Inoltre, la factory può gestire configurazioni basate su file YAML o variabili d’ambiente, rendendo il comportamento del sistema adattabile a diversi contesti (sviluppo, staging, produzione) senza modificare il codice di business.
Anche in contesti dove la flessibilità è una priorità, la factory offre una disciplina che previene la dispersione della logica di creazione e mantiene il codice più leggibile.
Implementazione in C++
In C++, la factory assume anche un ruolo cruciale per la gestione della memoria. Utilizzando std::unique_ptr, la factory garantisce che l’ownership dell’oggetto venga trasferita correttamente, evitando leak e facilitando il controllo del ciclo di vita.
Il pattern è particolarmente utile quando si combinano classi astratte con implementazioni concrete che richiedono risorse di sistema (thread, file handle, socket). Centralizzare la creazione consente di applicare politiche di allocazione uniformi e di gestire eccezioni in modo consistente.
Infine, la factory rende più semplice estendere il sistema con nuove classi Car (ad esempio HybridCar) senza introdurre dipendenze dirette nel codice client.
Questa implementazione dimostra come il Factory Pattern possa contribuire sia alla pulizia architetturale sia alla sicurezza della gestione delle risorse.
Quando usarlo davvero e quando stai solo complicando le cose
Il Factory Pattern è consigliato quando la creazione degli oggetti dipende da variabili di contesto, da configurazioni ambientali o da un numero crescente di implementazioni. In questi casi, la factory fornisce un punto unico di modifica, riducendo il debito tecnico e preparando il codice a cambiamenti futuri.
Se, invece, il progetto ha una singola implementazione stabile, senza prospettive di evoluzione, introdurre una factory può essere un caso di overengineering. In questi scenari, il costo aggiuntivo di un livello di astrazione supera i benefici, rallentando lo sviluppo e aumentando la complessità percepita.
La chiave è valutare attentamente il rapporto fra benefici e costi: se la probabilità di dover sostituire o estendere una classe è alta, il pattern è una scelta vincente; se è bassa, è più sensato mantenere un approccio diretto. Riconoscere il problema reale che il Factory Pattern risolve è la differenza tra una progettazione sana e un code smell di complessità inutile.