Abbiamo iniziato un viaggio per scoprire il modo migliore di strutturare il nostro codice ed evitare alcuni tipi di errori o di smells; questo viaggio continua con l’analisi dei design pattern organizzati per categoria.
Quando si progetta un’applicazione, una delle fasi più cruciali è quella della progettazione e istanziazione delle classi. Questo è dove entrano in gioco i design pattern creazionali i quali forniscono un modo efficace per gestire il processo di creazione di oggetti, fornendo allo sviluppatore un’astrazione dal processo di creazione, composizione e rappresentazione degli oggetti utilizzati.
Analizziamo alcuni pattern cercando di comprendere il vantaggio di utilizzarli seguendo questo schema: scopo, vantaggi, esempio in Java con relativa spiegazione.
Abstract factory
L’Abstract Factory è un pattern creazionale che fornisce un’interfaccia per la creazione di famiglie di oggetti correlati o dipendenti senza specificarne le classi concrete. Questo permette di creare oggetti correlati senza dover conoscere le loro classi specifiche.
Vantaggi:
- Incapsulamento delle famiglie di oggetti: L’Abstract Factory incapsula i dettagli di implementazione delle famiglie di oggetti, permettendo di modificare le famiglie di oggetti senza dover modificare il codice client.
- Isolamento del codice client: Il client può interagire con le famiglie di oggetti attraverso interfacce astratte, senza dover conoscere le implementazioni concrete.
Di seguito un esempio: consideriamo una fabbrica di mobili che produca tavoli e sedie:
// Interfaccia per i mobili
interface Furniture {
void assemble();
}
// Implementazione concreta di una sedia
class Chair implements Furniture {
@Override
public void assemble() {
System.out.println("Assemblaggio di una sedia");
}
}
// Implementazione concreta di un tavolo
class Table implements Furniture {
@Override
public void assemble() {
System.out.println("Assemblaggio di un tavolo");
}
}
// Interfaccia per la factory dei mobili
interface FurnitureFactory {
Furniture createFurniture();
}
// Factory per la creazione di sedie
class ChairFactory implements FurnitureFactory {
@Override
public Furniture createFurniture() {
return new Chair();
}
}
// Factory per la creazione di tavoli
class TableFactory implements FurnitureFactory {
@Override
public Furniture createFurniture() {
return new Table();
}
}
// Client che utilizza le factory per creare mobili
public class UseFactoryExample {
public static void main(String[] args) {
FurnitureFactory chairFactory = new ChairFactory();
Furniture chair = chairFactory.createFurniture();
chair.assemble();
FurnitureFactory tableFactory = new TableFactory();
Furniture table = tableFactory.createFurniture();
table.assemble();
}
}
Code language: PHP (php)
In questo esempio, abbiamo un’interfaccia Furniture che definisce un metodo assemble() comune per tutti i tipi di mobili.
Le classi concrete Chair e Table implementano questa interfaccia e forniscono l’implementazione specifica per il metodo assemble().
Successivamente, abbiamo un’interfaccia FurnitureFactory che dichiara un metodo createFurniture() per la creazione di mobili; le classi concrete ChairFactory e TableFactory implementano questa interfaccia e forniscono l’implementazione per creare istanze specifiche di sedie e tavoli.
Infine, nel metodo main() della classe UseFactoryExample, utilizziamo le factory per creare istanze di sedie e tavoli senza dover conoscere le classi concrete Chair e Table.
Questo ci consente di creare mobili in modo flessibile e senza dover modificare il codice client ogni volta che aggiungiamo nuovi tipi di mobili alla nostra applicazione.
Builder
Il pattern Builder è utile quando si desidera costruire oggetti complessi passo dopo passo, consentendo la creazione di diverse rappresentazioni dello stesso oggetto utilizzando lo stesso codice di costruzione.
Vantaggi:
- Separazione della costruzione dall’oggetto finale: Il Builder separa il processo di costruzione dell’oggetto dal suo risultato finale, consentendo la creazione di oggetti complessi con un’interfaccia chiara.
- Controllo preciso sulla costruzione: Il Builder permette di configurare un oggetto passo dopo passo, fornendo un controllo preciso sul processo di costruzione.
Facciamo un esempio, consideriamo un costruttore di pizze:
// Classe per rappresentare la pizza
class Pizza {
private String impasto;
private List<String> ingredienti;
public Pizza() {
this.ingredienti = new ArrayList<>();
}
public void setImpasto(String impasto) {
this.impasto = impasto;
}
public List<String> addIngrediente(String ingrediente) {
this.ingredienti.add(ingrediente);
return this.ingredienti;
}
public void showPizza() {
System.out.println("Pizza con impasto: " + impasto +
", ingredienti: " + ingredienti);
}
}
// Interfaccia per il builder della pizza
interface PizzaBuilder {
void buildImpasto();
void buildIngredienti();
Pizza getPizza();
}
// Implementazione concreta del builder per una pizza piccante
class FourSeasonPizzaBuilder implements PizzaBuilder {
private Pizza pizza;
public FourSeasonPizzaBuilder() {
this.pizza = new Pizza();
}
@Override
public void buildImpasto() {
pizza.setImpasto("Napoletano");
}
@Override
public void buildIngredienti() {
pizza.addIngrediente("olive");
pizza.addIngrediente("carciofi");
pizza.addIngrediente("prosciutto");
pizza.addIngrediente("funghi");
}
@Override
public Pizza getPizza() {
return pizza;
}
}
// Direttore per gestire il processo di costruzione della pizza
class PizzaDirector {
private PizzaBuilder pizzaBuilder;
public void setPizzaBuilder(PizzaBuilder pizzaBuilder) {
this.pizzaBuilder = pizzaBuilder;
}
public Pizza getPizza() {
return pizzaBuilder.getPizza();
}
public void constructPizza() {
pizzaBuilder.buildImpasto();
pizzaBuilder.buildIngredienti();
}
}
// Cliente che utilizza il builder per creare una pizza
public class Pizzeria {
public static void main(String[] args) {
PizzaDirector director = new PizzaDirector();
PizzaBuilder fourSeasonPizzaBuilder = new FourSeasonPizzaBuilder();
director.setPizzaBuilder(fourSeasonPizzaBuilder);
director.constructPizza();
Pizza fSPizza = director.getPizza();
fSPizza.showPizza();
}
}
Code language: PHP (php)
In questo esempio abbiamo una classe Pizza che rappresenta una pizza e ha vari attributi come impasto e ingredienti. Abbiamo anche un’interfaccia PizzaBuilder che dichiara i metodi per costruire la pizza passo dopo passo.
La classe FourSeasonPizzaBuilder è un’implementazione concreta dell’interfaccia PizzaBuilder e si occupa di costruire una pizza quattro stagioni con impasto napoletano, e gli ingredienti tipici della quattro stagioni. La classe PizzaDirector gestisce il processo di costruzione della pizza utilizzando il builder. Il client, nella classe Pizzeria, utilizza il director per specificare il tipo di pizza che desidera e quindi ottiene la pizza completata. Questo permette al client di creare una pizza personalizzata specificando solo gli ingredienti desiderati, senza dover gestire direttamente il processo di costruzione della pizza.
Singleton
Il pattern Singleton è progettato per garantire che una classe abbia una sola istanza e fornire un punto di accesso globale a tale istanza. Questo significa che ogni volta che viene richiesta un’istanza della classe Singleton, viene restituita sempre la stessa istanza, evitando la creazione di nuove istanze.
Vantaggi:
- Controllo sull’istanza: Il pattern Singleton offre un controllo completo sull’istanza della classe, garantendo che esista una sola istanza.
- Risparmio di memoria: Evita sprechi di memoria poiché viene creata una sola istanza dell’oggetto e viene riutilizzata ogni volta che necessaria.
- Accesso globale: Fornisce un punto di accesso globale all’istanza della classe Singleton, consentendo l’accesso da qualsiasi parte dell’applicazione.
Facciamo un esempio di come si possa creare un singleton:
// Classe Singleton per il gestore delle connessioni al database
public class DatabaseConnectionManager {
private static DatabaseConnectionManager instance;
private Connection connection;
// Costruttore privato
private DatabaseConnectionManager() {
// Inizializzazione della connessione al database
// ...
}
// Metodo statico per ottenere l'istanza Singleton
public static synchronized DatabaseConnectionManager getInstance() {
if (instance == null) {
instance = new DatabaseConnectionManager();
}
return instance;
}
// Metodo per ottenere la connessione al database
public Connection getConnection() {
// Restituisci la connessione
return connection;
}
// Altri metodi per gestire le connessioni al database
// ...
}
// Classe principale per il test del Singleton del gestore delle connessioni al database
public class Main {
public static void main(String[] args) {
// Ottenere l'istanza Singleton del gestore delle connessioni al database
DatabaseConnectionManager dbManager1 =
DatabaseConnectionManager.getInstance();
DatabaseConnectionManager dbManager2 =
DatabaseConnectionManager.getInstance();
// Verifica se le due istanze sono uguali
if (dbManager1 == dbManager2) {
System.out.println("Le due istanze del gestore delle
connessioni al database sono uguali.
Singleton funziona correttamente.");
} else {
System.out.println("Le due istanze del gestore delle
connessioni al database non sono uguali.
Qualcosa non va con Singleton.");
}
}
}
Code language: PHP (php)
L’esecuzione del codice sopra produce il seguente risultato
Entrambi i riferimenti puntano alla stessa istanza del Singleton.
La classe DatabaseConnectionManager ha un costruttore privato per evitare che venga istanziata direttamente: il metodo getInstance() è dichiarato come static e synchronized per garantire che solo un thread alla volta possa accedere al suo interno. Questo metodo restituisce sempre la stessa istanza di DatabaseConnectionManager, se l’istanza non è stata ancora creata, viene istanziata e restituita, altrimenti viene restituita direttamente. Nel metodo main() della classe Main, otteniamo due istanze di DatabaseConnectionManager utilizzando il metodo getInstance(). Poiché il Singleton garantisce che solo un’istanza della classe esista nell’applicazione, dbManager1 e dbManager2 saranno entrambe riferimenti alla stessa istanza, possiamo verificarlo confrontando i due riferimenti: se entrambi puntano alla stessa istanza, stampiamo un messaggio che conferma che il Singleton funziona correttamente.
In questo modo, il pattern Singleton garantisce che l’accesso al gestore delle connessioni al database sia centralizzato e che non ci siano più istanze di questo gestore nell’applicazione. Questo è utile per garantire l’efficienza delle risorse e mantenere la coerenza nella gestione delle connessioni al database.
Leggi anche: design pattern applicati ai microservizi
Conclusioni
Ci sono altri design pattern creazionali, questo articolo ha uno scopo dimostrativo e non esaustivo di tutti i pattern e vuole fornire delle indicazioni esplicative per quanto riguarda i pattern spiegati qui dando modo di analizzare sia questi che gli altri pattern creazionali perchè ha fornito degli strumenti di analisi: che cosa sono? come funzionano? come si possono usare?
Il viaggio verso la conoscenza dei pattern è ancora lungo; nel prossimo articolo vedremo gli strutturali che hanno un altro scopo rispetto ai creazionali.