Design pattern creazionali¶
I pattern creazionali spiegano come creare oggetti senza legare il codice alle classi concrete. Il codice che usa un oggetto conosce solo un'interfaccia; chi decide quale classe istanziare sta in un punto solo, facile da cambiare e da testare.
| Pattern | Problema che risolve | Usalo quando |
|---|---|---|
| Factory Method | Il codice fa new su classi concrete e non si estende |
Devi aggiungere nuove varianti senza toccare chi le usa |
| Abstract Factory | Servono famiglie di oggetti coerenti fra loro | Hai più prodotti che devono "andare insieme" |
| Builder | Costruttori con troppi parametri, molti opzionali | Un oggetto si costruisce a passi, in varianti diverse |
| Prototype | Creare copie di oggetti complessi senza conoscerne la classe | Clonare costa meno che costruire da zero |
| Singleton | Una sola istanza condivisa e un accesso globale | Risorsa davvero unica, e sai accettare i rischi |
Cosa sono¶
Un pattern creazionale separa due decisioni: quale oggetto creare e come usarlo. Il codice cliente lavora con un'interfaccia (Protocol o ABC in Python) e delega la creazione a una fabbrica, a un costruttore a passi o a un clone. Così puoi aggiungere una nuova classe senza modificare chi la usa.
Approfondimento con analogie e codice in più linguaggi: refactoring.guru/design-patterns/creational-patterns.
flowchart LR
cliente(["🧑 Codice cliente"])
subgraph uno["🔨 Un oggetto alla volta"]
fm["🏭 Factory Method"]
pr["🧬 Prototype"]
end
subgraph molti["📦 Oggetti complessi o in famiglia"]
af["🏬 Abstract Factory"]
bu["🧱 Builder"]
end
subgraph unico["🔒 Istanza unica"]
sg["🔑 Singleton"]
end
cliente -->|"nuova variante"| fm
cliente -->|"copia di uno esistente"| pr
cliente -->|"famiglia coerente"| af
cliente -->|"tanti parametri"| bu
cliente -->|"risorsa condivisa"| sg
Factory Method¶
In una frase: Un metodo che crea un oggetto, con sottoclassi che decidono quale classe concreta istanziare.
Problema che risolve: Il servizio Pagamenti nasce con Stripe e fa StripeProvider() ovunque. Arriva PayPal: devi toccare ogni punto che crea il provider e riempire il codice di if. Ogni nuovo gateway rompe di nuovo tutto.
Come funziona:
classDiagram
class PaymentProvider {
<<interface>>
+charge(amount) str
}
class StripeProvider {
+charge(amount) str
}
class PayPalProvider {
+charge(amount) str
}
class Checkout {
<<abstract>>
+create_provider() PaymentProvider
+pay(amount) str
}
class StripeCheckout {
+create_provider() PaymentProvider
}
class PayPalCheckout {
+create_provider() PaymentProvider
}
PaymentProvider <|.. StripeProvider
PaymentProvider <|.. PayPalProvider
Checkout <|-- StripeCheckout
Checkout <|-- PayPalCheckout
Checkout ..> PaymentProvider : crea
Checkoutdefinisce la logica comune (pay) e dichiara un metodo astrattocreate_provider.paychiamacreate_providersenza sapere cosa torna, solo che rispettaPaymentProvider.- Ogni sottoclasse (
StripeCheckout,PayPalCheckout) decide quale provider concreto istanziare. - Per aggiungere un gateway crei una sottoclasse nuova: il codice esistente non cambia.
Analogia: una pizzeria ha una ricetta unica per "preparare l'ordine", ma ogni sede sceglie il forno che usa.
Quando usarlo: - Non sai in anticipo quali classi concrete servono, e ne arriveranno altre. - Vuoi che la logica comune resti in un posto e cambi solo il "pezzo" creato. - Vuoi testare la logica con un provider finto, senza toccare la rete.
Quando NON usarlo / rischi: - Hai una sola classe concreta e nessun piano di aggiungerne: è solo indirezione. - Una gerarchia di sottoclassi per ogni variante può crescere troppo; in Python spesso basta passare una funzione o una classe come parametro.
Esempio pratico:
from typing import Protocol
from abc import ABC, abstractmethod
class PaymentProvider(Protocol):
def charge(self, amount: int) -> str: ...
class StripeProvider:
def charge(self, amount: int) -> str: return f"stripe:{amount}"
class PayPalProvider:
def charge(self, amount: int) -> str: return f"paypal:{amount}"
class Checkout(ABC):
@abstractmethod
def create_provider(self) -> PaymentProvider: ...
def pay(self, amount: int) -> str:
return self.create_provider().charge(amount)
class StripeCheckout(Checkout):
def create_provider(self) -> PaymentProvider: return StripeProvider()
Pattern correlati: Abstract Factory (una famiglia di Factory Method), Template Method (stessa struttura: il passo variabile è la creazione), Prototype (alternativa senza sottoclassi).
Abstract Factory¶
In una frase: Un'interfaccia per creare famiglie di oggetti correlati senza indicare le classi concrete.
Problema che risolve: Il servizio Notifiche deve inviare una conferma d'ordine e un promemoria di spedizione. Via email serve un template HTML, via SMS un testo corto. Se mischi un template email con un canale SMS il messaggio esce rotto. Vuoi garantire che tutti gli oggetti di un invio siano dello stesso canale.
Come funziona:
flowchart LR
notif["✉️ Servizio Notifiche"]
subgraph email["📧 EmailFactory"]
ec["📄 Conferma HTML"]
er["📄 Promemoria HTML"]
end
subgraph sms["📱 SmsFactory"]
sc["💬 Conferma 160 car."]
sr["💬 Promemoria 160 car."]
end
cliente(["🧑 Cliente"])
notif -->|"canale email"| email
notif -->|"canale sms"| sms
email -->|"famiglia coerente"| cliente
sms -->|"famiglia coerente"| cliente
classDiagram
class NotificationFactory {
<<interface>>
+create_confirmation() Message
+create_reminder() Message
}
class EmailFactory {
+create_confirmation() Message
+create_reminder() Message
}
class SmsFactory {
+create_confirmation() Message
+create_reminder() Message
}
class Message {
<<interface>>
+render(order_id) str
}
class EmailConfirmation
class EmailReminder
class SmsConfirmation
class SmsReminder
NotificationFactory <|.. EmailFactory
NotificationFactory <|.. SmsFactory
Message <|.. EmailConfirmation
Message <|.. EmailReminder
Message <|.. SmsConfirmation
Message <|.. SmsReminder
EmailFactory ..> EmailConfirmation : crea
SmsFactory ..> SmsConfirmation : crea
NotificationFactorydichiara un metodo per ogni prodotto della famiglia (conferma, promemoria).EmailFactoryeSmsFactoryimplementano tutti i metodi e restituiscono solo oggetti del proprio canale.- Il cliente riceve una factory e chiama i metodi: non sa mai se sta lavorando con email o SMS.
- Il canale si sceglie una volta sola, quando scegli la factory. Gli oggetti sono coerenti per costruzione.
Analogia: un negozio di mobili vende sedie, tavoli e divani in stile moderno o classico; scegli lo stile e ricevi tutti i pezzi abbinati.
Quando usarlo: - Il codice deve lavorare con più famiglie di prodotti e i prodotti di una famiglia devono stare insieme. - Vuoi cambiare famiglia (canale, tema, fornitore) in un punto solo, per esempio in configurazione.
Quando NON usarlo / rischi: - Hai un solo prodotto per famiglia: basta un Factory Method. - Aggiungere un nuovo tipo di prodotto obbliga a modificare l'interfaccia e tutte le factory concrete.
Esempio pratico:
from typing import Protocol
class Message(Protocol):
def render(self, order_id: str) -> str: ...
class EmailConfirmation:
def render(self, order_id: str) -> str: return f"<h1>Ordine {order_id}</h1>"
class SmsConfirmation:
def render(self, order_id: str) -> str: return f"Ordine {order_id} ok"
class NotificationFactory(Protocol):
def create_confirmation(self) -> Message: ...
class EmailFactory:
def create_confirmation(self) -> Message: return EmailConfirmation()
class SmsFactory:
def create_confirmation(self) -> Message: return SmsConfirmation()
def notify(factory: NotificationFactory, order_id: str) -> str:
return factory.create_confirmation().render(order_id)
Pattern correlati: Factory Method (spesso usato dentro ogni metodo della factory), Builder (costruisce un oggetto complesso a passi, invece di una famiglia), Singleton (la factory concreta è spesso una sola), Bridge.
Builder¶
In una frase: Costruisce un oggetto complesso passo per passo, separando la costruzione dalla rappresentazione.
Problema che risolve: Un Order ha cliente, righe, indirizzo di spedizione, codice sconto, note, regalo, fattura. Il costruttore ha dieci parametri, quasi tutti opzionali, e le chiamate diventano Order(c, items, None, None, True, None, ...): illeggibili e facili da sbagliare.
Come funziona:
flowchart LR
cliente(["🧑 Cliente"])
subgraph builder["🧱 OrderBuilder"]
s1["🛒 add_item"] --> s2["🎟️ with_coupon"] --> s3["🎁 as_gift"]
end
ordine["📄 Order immutabile"]
mag["🏭 Magazzino"]
cliente -->|"compone a passi"| builder
s3 -->|"build()"| ordine
ordine -->|"pronto"| mag
classDiagram
class OrderBuilder {
-customer str
-items list
-coupon str
-gift bool
+add_item(sku, qty) OrderBuilder
+with_coupon(code) OrderBuilder
+as_gift() OrderBuilder
+build() Order
}
class Order {
+customer str
+items tuple
+coupon str
+gift bool
}
class CheckoutService {
+create_order() Order
}
OrderBuilder ..> Order : build
CheckoutService ..> OrderBuilder : usa
- Il builder tiene lo stato parziale dell'ordine mentre lo componi.
- Ogni metodo (
add_item,with_coupon,as_gift) imposta una parte e restituisce il builder, così le chiamate si concatenano. build()valida (almeno una riga, cliente presente) e restituisce unOrderimmutabile.- Il cliente legge una sequenza di passi con nomi chiari invece di una lista di posizionali.
Analogia: ordini un panino al banco: scegli pane, farcitura, salse, una cosa alla volta; alla fine ti consegnano il panino completo.
Quando usarlo: - L'oggetto ha molti parametri opzionali o va costruito in più passi con validazione finale. - Vuoi produrre varianti diverse dello stesso oggetto con lo stesso processo (ordine normale, ordine regalo, ordine aziendale).
Quando NON usarlo / rischi:
- L'oggetto ha tre o quattro campi: in Python bastano dataclass e argomenti keyword-only.
- Il builder mutabile può essere riusato per errore fra due ordini: fai in modo che build() restituisca sempre un oggetto nuovo e immutabile.
Esempio pratico:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Order:
customer: str
items: tuple[tuple[str, int], ...]
coupon: str | None = None
gift: bool = False
class OrderBuilder:
def __init__(self, customer: str) -> None:
self._customer, self._items = customer, []
self._coupon: str | None = None
self._gift = False
def add_item(self, sku: str, qty: int) -> "OrderBuilder":
self._items.append((sku, qty)); return self
def with_coupon(self, code: str) -> "OrderBuilder":
self._coupon = code; return self
def as_gift(self) -> "OrderBuilder":
self._gift = True; return self
def build(self) -> Order:
if not self._items: raise ValueError("ordine vuoto")
return Order(self._customer, tuple(self._items), self._coupon, self._gift)
Uso: OrderBuilder("anna").add_item("SKU-1", 2).with_coupon("WELCOME").build().
Pattern correlati: Abstract Factory (restituisce subito il prodotto, il Builder lo costruisce a passi), Composite (il Builder spesso costruisce alberi), Prototype.
Prototype¶
In una frase: Crea nuovi oggetti copiando un'istanza esistente, senza dipendere dalla sua classe.
Problema che risolve: Il Catalogo ha prodotti configurati con decine di attributi, varianti e prezzi per paese. Per creare la taglia "L" a partire dalla "M" dovresti leggere tutti i campi uno per uno, incluso quelli privati, e conoscere la classe esatta (prodotto semplice, bundle, abbonamento).
Come funziona:
flowchart LR
db[("🗄️ DB Prodotti")]
cat["🛍️ Servizio Catalogo"]
base["👕 T-shirt M, prezzo, attributi"]
copia["👕 T-shirt L (copia)"]
db -->|"1. carica il modello"| cat
cat -->|"2. clone()"| base
base -->|"3. copia completa"| copia
copia -->|"4. cambia solo la taglia"| cat
classDiagram
class Prototype {
<<interface>>
+clone() Prototype
}
class Product {
+sku str
+name str
+price int
+attributes dict
+clone() Product
}
class BundleProduct {
+children list
+clone() BundleProduct
}
class CatalogService {
+create_variant(base, sku) Product
}
Prototype <|.. Product
Product <|-- BundleProduct
CatalogService ..> Prototype : clone
- L'interfaccia
Prototypeespone un solo metodo:clone(). - Ogni classe sa copiare sé stessa, campi privati compresi, e decide se la copia è profonda.
CatalogServiceprende un prodotto "modello", lo clona e cambia solo i campi che differiscono.- Il servizio non conosce la classe concreta: un bundle si clona come un prodotto semplice.
Analogia: la divisione cellulare: la cellula si copia da sola, con tutto il suo contenuto, senza che qualcuno la ricostruisca dall'esterno.
Quando usarlo: - Costruire l'oggetto da zero costa (query, calcoli) e hai già un'istanza simile. - Vuoi ridurre le sottoclassi che esistono solo per preimpostare valori: tieni un registro di prototipi pronti. - Il codice non deve dipendere dalla classe concreta dell'oggetto da copiare.
Quando NON usarlo / rischi:
- Oggetti con riferimenti circolari o risorse esterne (connessioni, file): la copia profonda è fragile.
- Copia superficiale per errore: due prodotti condividono lo stesso dict di attributi e si modificano a vicenda.
Esempio pratico:
import copy
from dataclasses import dataclass, field, replace
from typing import Protocol, Self
class Prototype(Protocol):
def clone(self) -> Self: ...
@dataclass
class Product:
sku: str
name: str
price: int
attributes: dict[str, str] = field(default_factory=dict)
def clone(self) -> Self:
return copy.deepcopy(self)
base = Product("TSHIRT-M", "T-shirt", 1990, {"size": "M", "color": "blue"})
large = base.clone()
large.sku, large.attributes["size"] = "TSHIRT-L", "L"
# con dataclass immutabili: replace(base, sku="TSHIRT-L")
Pattern correlati: Abstract Factory (può usare prototipi al posto di sottoclassi), Memento (anche lui salva copie di stato), Composite e Decorator (strutture complesse che conviene clonare).
Singleton¶
In una frase: Garantisce che una classe abbia una sola istanza e offre un punto di accesso globale.
Problema che risolve: Il servizio Ordini apre un pool di connessioni al database. Se ogni modulo ne crea uno, il database si satura. Serve un'istanza condivisa e un modo per raggiungerla da ovunque.
Come funziona:
flowchart LR
ordini["📦 OrderRepository"]
pag["💳 PaymentRepository"]
pool{{"🔒 ConnectionPool, una sola istanza"}}
db[("🗄️ Database")]
test(["🧪 Test"])
ordini -->|"get_instance()"| pool
pag -->|"get_instance()"| pool
pool --> db
test -.->|"difficile da sostituire"| pool
classDiagram
class ConnectionPool {
-instance ConnectionPool$
-ConnectionPool()
+get_instance() ConnectionPool$
+acquire() Connection
}
class OrderRepository {
+save(order)
}
class PaymentRepository {
+save(payment)
}
OrderRepository ..> ConnectionPool : get_instance
PaymentRepository ..> ConnectionPool : get_instance
- Il costruttore è nascosto: nessuno può fare
ConnectionPool()dall'esterno. - Il metodo statico
get_instance()crea l'istanza la prima volta e poi restituisce sempre la stessa. - Tutti i repository chiamano
get_instance()e condividono lo stesso pool. - In Python il "singleton" naturale è un oggetto a livello di modulo: il modulo viene importato una sola volta.
Analogia: un paese ha un solo governo: chiunque dica "il governo" si riferisce alla stessa entità.
Quando usarlo: - La risorsa è davvero unica per processo (pool di connessioni, cache in memoria, configurazione caricata). - Vuoi controllare l'inizializzazione pigra di una risorsa costosa.
Quando NON usarlo / rischi: - È stato globale travestito: chi lo usa nasconde una dipendenza e i test non possono sostituirla senza trucchi (monkeypatch, reset manuali fra un test e l'altro). - Viola la responsabilità singola: la classe gestisce sia il proprio compito sia il proprio ciclo di vita. - Con thread e processi multipli l'istanza "unica" non lo è più: serve un lock o una strategia per processo. - Alternativa preferibile: crea l'istanza una volta sola all'avvio (composition root) e passala come dipendenza. Ottieni l'unicità senza l'accesso globale, e nei test passi un finto.
Esempio pratico:
from typing import Protocol
class ConnectionPool:
_instance: "ConnectionPool | None" = None
@classmethod
def get_instance(cls) -> "ConnectionPool":
if cls._instance is None:
cls._instance = cls()
return cls._instance
def acquire(self) -> str: return "conn"
# Alternativa con dependency injection: unica istanza, nessun accesso globale.
class Pool(Protocol):
def acquire(self) -> str: ...
class OrderRepository:
def __init__(self, pool: Pool) -> None: self._pool = pool
def save(self, order_id: str) -> str: return f"{self._pool.acquire()}:{order_id}"
repo = OrderRepository(ConnectionPool()) # creato una volta sola all'avvio
Pattern correlati: Abstract Factory, Builder e Prototype (le loro istanze sono spesso singleton), Facade (spesso una sola istanza), Flyweight (molte istanze condivise, non una).
Factory Method vs Abstract Factory vs Builder¶
| Pattern | Cosa crea | Come decide | Quando preferirlo |
|---|---|---|---|
| Factory Method | Un oggetto | Una sottoclasse sovrascrive un metodo | Una sola gerarchia di prodotti, varianti che crescono |
| Abstract Factory | Una famiglia di oggetti coerenti | Scegli la factory concreta una volta | Più prodotti che devono stare insieme (canale, tema) |
| Builder | Un oggetto complesso, a passi | Il cliente chiama i passi che vuole | Molti parametri opzionali, validazione finale |
Come scegliere¶
flowchart TD
q1{"Serve una sola istanza condivisa?"}
q2{"Oggetto con molti parametri opzionali?"}
q3{"Hai gia' un'istanza simile da copiare?"}
q4{"Devi creare piu' oggetti che vanno insieme?"}
di["Crea all'avvio e passa come dipendenza"]
sg["Singleton, solo se la DI non basta"]
bu["Builder"]
pr["Prototype"]
af["Abstract Factory"]
fm["Factory Method"]
q1 -- si --> di
di --> sg
q1 -- no --> q2
q2 -- si --> bu
q2 -- no --> q3
q3 -- si --> pr
q3 -- no --> q4
q4 -- si --> af
q4 -- no --> fm