W3docs

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:

FamigliaProblemaEsempi
CreazionaliCome vengono creati gli oggettiSingleton, Factory, Builder
StrutturaliCome vengono composti gli oggettiAdapter, Decorator, Facade
ComportamentaliCome interagiscono gli oggettiStrategy, 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.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Ogni livello stampa un totale diverso — regular rimane $100.00, silver scende a $90.00, gold a $80.00, coupon a $95.00 — anche se Checkout.total chiama lo stesso singolo metodo.
  • Il contesto Checkout non si ramifica mai sul livello stesso; delega semplicemente a qualsiasi DiscountStrategy contenga 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 a Checkout.
  • Le righe finali confermano la strategia attiva: dopo aver selezionato nuovamente gold, la policy legge 20.0% off e il totale è $80.00, dimostrando che il contesto riflette qualsiasi strategia sia stata impostata per ultima.

Esercitati

Pratica
Quale problema risolve il pattern Strategy?
Quale problema risolve il pattern Strategy?
Was this page helpful?