Introduzione ai Design Pattern in Java
Introduzione ai design pattern in Java: cosa sono e come usare i più comuni, con esempi pratici.
Un design pattern è una soluzione riutilizzabile e con un nome preciso a un problema ricorrente nello sviluppo software. I pattern non sono librerie da importare né codice da copiare — sono schemi per organizzare classi e oggetti che gli sviluppatori esperti hanno consolidato nel tempo. Impararli ti fornisce un vocabolario condiviso: dire "useremo una Factory qui" e un altro sviluppatore Java capirà immediatamente di cosa si tratta.
Questa pagina introduce cosa sono i design pattern, le tre famiglie in cui si suddividono e tre dei più comuni — Strategy, Factory e Singleton — con un esempio eseguibile che li combina. Si presuppone che tu abbia familiarità con le interfacce e il polimorfismo, poiché quasi ogni pattern si basa su di essi.
I pattern sono stati resi popolari dal libro del 1994 Design Patterns scritto dalla "Gang of Four", che ne catalogava 23. Non è necessario conoscerli tutti e 23 per cominciare. Un gruppo ristretto, applicato al momento giusto, rende il codice più facile da modificare senza romperlo.
Le tre famiglie
Il catalogo classico suddivide i pattern in tre gruppi in base a ciò con cui aiutano:
| Famiglia | Problema | Esempi |
|---|---|---|
| Creazionali | Come vengono creati gli oggetti | Singleton, Factory, Builder |
| Strutturali | Come vengono composti gli oggetti | Adapter, Decorator, Facade |
| Comportamentali | Come interagiscono gli oggetti | Strategy, Observer, Iterator |
Java stesso è pieno di questi. StringBuilder è un Builder, Iterator è il pattern Iterator e java.util.logging usa i Singleton. Stavi già usando i pattern senza nominarli.
Strategy: sostituire l'algoritmo
Il pattern Strategy incapsula una famiglia di algoritmi intercambiabili dietro un'interfaccia comune, così il codice chiamante può passare da uno all'altro senza cambiare. Si definisce l'interfaccia, si scrive ogni variante come classe propria e si lascia che un contesto tenga quella di cui ha bisogno.
interface DiscountStrategy {
double apply(double price);
}
class PercentOff implements DiscountStrategy {
private final double percent;
PercentOff(double percent) { this.percent = percent; }
public double apply(double price) { return price * (1 - percent / 100); }
}Una classe contesto tiene una DiscountStrategy e delega a essa, invece di ramificarsi con una catena if/switch. Aggiungere un nuovo sconto significa aggiungere una classe — non modificare il codice esistente. (Il contesto Checkout completo, insieme alle varianti NoDiscount e FlatOff, appaiono nell'esempio eseguibile qui sotto.)
Factory: centralizzare la creazione
Una Factory è un singolo metodo (o classe) responsabile di decidere quale tipo concreto istanziare. I chiamanti richiedono un oggetto tramite una descrizione e ricevono un oggetto che soddisfa l'interfaccia, senza conoscere la classe esatta.
static DiscountStrategy forCustomer(String tier) {
return switch (tier) {
case "gold" -> new PercentOff(20);
case "silver" -> new PercentOff(10);
default -> new NoDiscount();
};
}La logica di creazione risiede in un unico posto. Se le regole cambiano, si modifica la factory — ogni chiamante continua a funzionare senza modifiche.
Singleton: esattamente un'istanza
Un Singleton garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale ad essa. In Java moderno un enum è il modo più semplice e thread-safe per crearne uno — la JVM garantisce che le sue costanti vengano create esattamente una volta. Consulta The Singleton Pattern per le varianti con inizializzazione pigra e double-checked locking.
enum Config {
INSTANCE;
private final String env = "production";
public String env() { return env; }
}
// usage
String e = Config.INSTANCE.env();Usa i Singleton con parsimonia — introducono stato globale, il che rende i test più difficili. Spesso un singolo oggetto passato tramite un costruttore (dependency injection) è la scelta migliore.
Quando non usare un pattern
I pattern aggiungono struttura, e la struttura ha un costo. Un switch con due casi non ha bisogno del pattern Strategy; una classe che si istanzia una sola volta non ha bisogno di una Factory. Usa un pattern quando senti il dolore che risolve — ramificazioni duplicate, chiamate new sparse, dipendenze tra oggetti aggrovigliate — non prima. Applicare i pattern in modo eccessivo produce codice più difficile da leggere del problema che si intendeva semplificare.
Strategy e Factory insieme
L'esempio eseguibile qui sotto combina due pattern. DiscountStrategy è l'interfaccia Strategy con tre implementazioni; forCustomer è una Factory che ne sceglie una. Il contesto Checkout delega a qualsiasi strategia contenga, quindi il suo codice non cambia mai al variare del comportamento di pricing.
Cosa trarre dall'esecuzione:
- Ogni livello stampa un totale diverso —
regularrimane $100.00,silverscende a $90.00,golda $80.00,coupona $95.00 — anche seCheckout.totalchiama lo stesso singolo metodo. - Il contesto
Checkoutnon si ramifica mai sul livello stesso; delega semplicemente a qualsiasiDiscountStrategycontenga al momento. - La Factory
forCustomerè l'unico posto che sa quale classe concreta corrisponde a quale livello, quindi la logica di selezione risiede in un unico posto. - Cambiare il comportamento è semplicemente
setStrategy(...)a runtime — aggiungere un quarto sconto significherebbe una nuova classe più un caso nella factory, senza alcuna modifica aCheckout. - Le righe finali confermano la strategia attiva: dopo aver selezionato nuovamente
gold, la policy legge20.0% offe il totale è $80.00, dimostrando che il contesto riflette qualsiasi strategia sia stata impostata per ultima.