Design pattern comportamentali¶
I pattern comportamentali descrivono come gli oggetti comunicano fra loro e come si dividono le responsabilità. Risolvono il problema di chi fa cosa, chi avvisa chi, e come cambiare un comportamento senza riscrivere il codice che lo usa.
| Pattern | Problema che risolve | Usalo quando |
|---|---|---|
| Chain of Responsibility | Una richiesta deve passare per più controlli in sequenza | Vuoi aggiungere o togliere controlli senza toccare il chiamante |
| Command | Un'operazione va salvata, accodata o annullata | Servono undo, coda di operazioni o log delle azioni |
| Iterator | Scorrere una collezione senza conoscerne la struttura interna | Hai strutture diverse da percorrere nello stesso modo |
| Mediator | Troppi oggetti che si parlano direttamente fra loro | Le dipendenze incrociate sono diventate ingestibili |
| Memento | Salvare e ripristinare lo stato di un oggetto | Serve uno snapshot senza esporre i campi interni |
| Observer | Avvisare più oggetti quando qualcosa cambia | Molti interessati a un evento, senza accoppiarli alla sorgente |
| State | Un oggetto cambia comportamento in base al suo stato | Hai tanti if stato == ... sparsi nel codice |
| Strategy | Scegliere un algoritmo fra tanti a runtime | Diverse varianti dello stesso calcolo, intercambiabili |
| Template Method | Stessa procedura con passi diversi nelle sottoclassi | Lo scheletro è fisso, cambiano solo alcuni passi |
| Visitor | Aggiungere operazioni a una gerarchia di classi senza modificarla | Nuove operazioni frequenti, nuove classi rare |
Cosa sono¶
Un programma a oggetti è fatto di oggetti che si scambiano messaggi. I pattern comportamentali dicono come organizzare questi scambi: chi tiene la logica, chi avvisa chi, come sostituire un comportamento con un altro senza cambiare chi lo usa. Lo scopo è ridurre l'accoppiamento fra gli oggetti e rendere ogni responsabilità facile da trovare.
Approfondimento con esempi in molti linguaggi: refactoring.guru/design-patterns.
flowchart TD
subgraph chi["🗣️ Chi parla con chi"]
obs["👀 Observer"]
med["🗼 Mediator"]
chain["⛓️ Chain of Responsibility"]
end
subgraph azioni["📝 Azioni e stato"]
cmd["🧾 Command"]
mem["📸 Memento"]
state["🔄 State"]
end
subgraph algo["🧩 Varianti di un algoritmo"]
strat["🚚 Strategy"]
tmpl["📋 Template Method"]
end
subgraph coll["📚 Collezioni e gerarchie"]
it["🧭 Iterator"]
vis["🧾 Visitor"]
end
Chain of Responsibility¶
In una frase: Una richiesta passa lungo una catena di gestori, e ognuno decide se trattarla o passarla al successivo.
Problema che risolve: Prima di accettare un ordine devi controllare che il cliente sia autenticato, che i prodotti siano disponibili, che il totale non superi il limite della carta. Se metti tutti i controlli in un unico metodo, ogni nuova regola fa crescere quel metodo. Se devi riusare solo alcuni controlli in un altro flusso, non puoi.
Come funziona:
flowchart LR
ordine["📦 Ordine 42"] --> auth["🔑 Controllo login"]
auth -->|"ok"| mag["🏭 Controllo giacenza"]
mag -->|"ok"| limite["💳 Controllo limite spesa"]
limite -->|"ok"| fine(["✅ Ordine accettato"])
auth -->|"non loggato"| ko(["⛔ Rifiutato"])
mag -->|"esaurito"| ko
limite -->|"troppo alto"| ko
sequenceDiagram
participant C as Servizio Ordini
participant A as Controllo Autenticazione
participant M as Controllo Magazzino
participant P as Controllo Pagamento
C->>A: valida(ordine)
A->>M: valida(ordine)
M->>P: valida(ordine)
P-->>M: ok
M-->>A: ok
A-->>C: ok
Ogni gestore ha la stessa interfaccia e un riferimento al gestore successivo. Il chiamante parla solo con il primo anello. Ogni anello fa il suo controllo: se fallisce, interrompe la catena e risponde subito. Se passa, chiama il successivo. L'ordine dei controlli è deciso componendo la catena, non dentro i gestori.
Analogia: l'assistenza clienti al telefono. Prima risponde il risponditore automatico, poi l'operatore di primo livello, poi il tecnico. Ogni livello risolve se può, altrimenti passa oltre.
Quando usarlo: - Una richiesta deve attraversare più controlli o trasformazioni in un ordine preciso. - Vuoi aggiungere, togliere o riordinare i controlli senza modificare il codice chiamante. - Non sai in anticipo quale gestore tratterà la richiesta.
Quando NON usarlo / rischi:
- Se i controlli sono due e non cambieranno, una sequenza di if è più chiara.
- Una richiesta può arrivare in fondo senza che nessuno la gestisca: decidi cosa succede in quel caso.
- La catena rende il flusso meno visibile: serve un punto dove si vede l'ordine completo.
Esempio pratico: Il servizio Ordini costruisce una catena di validatori: autenticazione, disponibilità in Magazzino, limite di spesa in Pagamenti. Ogni validatore è una classe piccola e testabile da sola.
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class Ordine:
cliente_id: int
totale: float
class Validatore(ABC):
def __init__(self, prossimo: "Validatore | None" = None) -> None:
self._prossimo = prossimo
def valida(self, ordine: Ordine) -> str | None:
errore = self._controlla(ordine)
if errore or self._prossimo is None:
return errore
return self._prossimo.valida(ordine)
@abstractmethod
def _controlla(self, ordine: Ordine) -> str | None: ...
class LimiteSpesa(Validatore):
def _controlla(self, ordine: Ordine) -> str | None:
return "totale troppo alto" if ordine.totale > 5000 else None
Pattern correlati: Command (i gestori possono essere comandi), Decorator (struttura simile, scopo diverso: aggiunge comportamento invece di scegliere chi gestisce), Mediator.
Command¶
In una frase: Trasforma una richiesta in un oggetto, così puoi salvarla, accodarla, annullarla.
Problema che risolve: Il carrello ha i pulsanti "aggiungi", "rimuovi", "cambia quantità" e il cliente vuole il tasto "annulla ultima azione". Se ogni pulsante chiama direttamente un metodo del carrello, non hai traccia di cosa è successo e non puoi tornare indietro.
Come funziona:
flowchart LR
cliente(["🧑 Cliente"]) -->|"1. clic Aggiungi"| btn["🔘 Pulsante"]
btn -->|"2. crea"| cmd["📝 Comando AggiungiProdotto"]
cmd -->|"3. esegui"| carrello["🛒 Carrello"]
cmd -->|"4. salva"| storico[["🗂️ Storico comandi"]]
storico -->|"annulla ultimo"| carrello
classDiagram
class Comando {
<<interface>>
+esegui()
+annulla()
}
class AggiungiProdotto {
-carrello
-prodotto
+esegui()
+annulla()
}
class Carrello {
+aggiungi(prodotto)
+rimuovi(prodotto)
}
class StoricoComandi {
-pila
+esegui(comando)
+annullaUltimo()
}
Comando <|.. AggiungiProdotto
AggiungiProdotto --> Carrello
StoricoComandi --> Comando
Ogni azione diventa una classe con esegui() e annulla(). Il comando conosce il destinatario (il carrello) e i parametri necessari. Un oggetto "storico" esegue i comandi e li mette in una pila. Per annullare, prende l'ultimo comando dalla pila e chiama annulla(). Il pulsante non sa niente del carrello: conosce solo il comando.
Analogia: al ristorante il cameriere scrive l'ordine su un foglio e lo passa in cucina. Il foglio è il comando: si può accodare, leggere più volte, stracciare.
Quando usarlo: - Serve annullare o ripetere operazioni. - Vuoi accodare operazioni, eseguirle più tardi o registrarle in un log. - Vuoi che chi avvia l'azione (pulsante, API, scheduler) non conosca chi la esegue.
Quando NON usarlo / rischi: - Per azioni semplici senza undo o coda è solo una classe in più per ogni metodo. - L'annullamento deve ripristinare lo stato in modo corretto: con operazioni che hanno effetti esterni (email inviate, pagamenti) non basta.
Esempio pratico: Il carrello del Catalogo registra ogni modifica come comando. Il cliente preme "annulla" e la quantità torna quella di prima. Lo stesso storico serve per ricostruire il carrello dopo un crash.
from typing import Protocol
class Comando(Protocol):
def esegui(self) -> None: ...
def annulla(self) -> None: ...
class Carrello:
def __init__(self) -> None:
self.righe: dict[str, int] = {}
class AggiungiProdotto:
def __init__(self, carrello: Carrello, sku: str, qta: int) -> None:
self._c, self._sku, self._qta = carrello, sku, qta
def esegui(self) -> None:
self._c.righe[self._sku] = self._c.righe.get(self._sku, 0) + self._qta
def annulla(self) -> None:
self._c.righe[self._sku] -= self._qta
class Storico:
def __init__(self) -> None:
self._pila: list[Comando] = []
def esegui(self, cmd: Comando) -> None:
cmd.esegui()
self._pila.append(cmd)
def annulla_ultimo(self) -> None:
self._pila.pop().annulla()
Pattern correlati: Memento (per salvare lo stato prima di eseguire, invece di calcolare l'inverso), Chain of Responsibility, Strategy (entrambi incapsulano un'azione, ma Strategy sceglie "come" fare una cosa, Command "quale" cosa fare).
Iterator¶
In una frase: Scorri gli elementi di una collezione senza sapere come è fatta dentro.
Problema che risolve: Il Catalogo tiene i prodotti in una lista, le categorie in un albero, i risultati di ricerca arrivano a pagine da un'API. Chi deve stampare o filtrare i prodotti non vuole scrivere tre cicli diversi. Vuole chiedere "il prossimo" e basta.
Come funziona:
flowchart LR
notif["✉️ Servizio Notifiche"] -->|"prossimo prodotto"| it["🧭 Iteratore pagine"]
it -->|"carica pagina N"| api>"☁️ API Catalogo"]
subgraph pagine["📚 Risultati a pagine"]
p1["📄 Pagina 1"]
p2["📄 Pagina 2"]
p3["📄 Pagina 3"]
end
api --> pagine
classDiagram
class Iteratore {
<<interface>>
+prossimo() Prodotto
+haProssimo() bool
}
class Collezione {
<<interface>>
+crea_iteratore() Iteratore
}
class CatalogoPaginato {
-pagina_corrente
+crea_iteratore() Iteratore
}
class IteratorePagine {
-catalogo
-indice
+prossimo() Prodotto
+haProssimo() bool
}
Collezione <|.. CatalogoPaginato
Iteratore <|.. IteratorePagine
CatalogoPaginato ..> IteratorePagine : crea
La collezione espone un metodo che restituisce un iteratore. L'iteratore sa come scorrere quella struttura specifica e tiene traccia della posizione. Il client usa solo haProssimo() e prossimo(). Puoi avere più iteratori attivi sulla stessa collezione, ognuno con la propria posizione. In Python il pattern è nel linguaggio: __iter__ e __next__, oppure i generatori.
Analogia: una guida turistica. Tu non conosci la città, lei ti porta da un posto al prossimo. Se cambi guida, cambi il percorso, ma tu fai sempre la stessa cosa: segui.
Quando usarlo: - La struttura interna della collezione è complessa o deve restare nascosta. - Vuoi più modi di percorrere la stessa collezione (per prezzo, per categoria, a pagine). - La collezione è troppo grande per caricarla tutta in memoria: l'iteratore carica un pezzo per volta.
Quando NON usarlo / rischi:
- Per una semplice lista Python, il for nativo basta.
- Modificare la collezione mentre la scorri dà risultati imprevedibili.
Esempio pratico: Il Catalogo espone i prodotti con un iteratore che chiama l'API una pagina per volta. Il servizio Notifiche, che manda le offerte, scorre tutti i prodotti con un normale for senza sapere che ci sono pagine.
from collections.abc import Iterator
from dataclasses import dataclass
@dataclass
class Prodotto:
sku: str
prezzo: float
class CatalogoPaginato:
def __init__(self, carica_pagina) -> None:
self._carica_pagina = carica_pagina
def __iter__(self) -> Iterator[Prodotto]:
numero = 1
while True:
pagina: list[Prodotto] = self._carica_pagina(numero)
if not pagina:
return
yield from pagina
numero += 1
# for prodotto in CatalogoPaginato(api.pagina): ...
Pattern correlati: Composite (l'iteratore percorre l'albero), Visitor (spesso usato insieme per applicare un'operazione a ogni elemento), Memento (per salvare la posizione dell'iteratore).
Mediator¶
In una frase: Gli oggetti non si parlano fra loro, parlano con un mediatore che coordina tutto.
Problema che risolve: Nella pagina di checkout il campo "indirizzo" aggiorna le spese di spedizione, le spese cambiano il totale, il totale abilita il pulsante "paga", il metodo di pagamento cambia le spese. Ogni componente conosce tutti gli altri. Cambiarne uno rompe gli altri e non puoi riusarlo altrove.
Come funziona:
flowchart LR
ind["🏠 Campo indirizzo"] -->|"cambiato"| med["🗼 Mediatore Checkout"]
med -->|"ricalcola"| spese["🚚 Calcolo spedizione"]
med -->|"aggiorna"| tot["💶 Totale"]
med -->|"abilita"| paga(["💳 Pulsante Paga"])
sequenceDiagram
participant I as Campo Indirizzo
participant M as Mediatore Checkout
participant S as Calcolo Spedizione
participant T as Totale
participant B as Pulsante Paga
I->>M: notifica cambiato
M->>S: ricalcola(indirizzo)
S-->>M: costo
M->>T: aggiorna(costo)
M->>B: abilita
Ogni componente conosce solo il mediatore e lo avvisa quando succede qualcosa. Il mediatore decide chi deve reagire e in che ordine. I componenti non sanno chi altro esiste. Tutta la logica di coordinamento sta in un posto solo. Se cambi la regola ("con ritiro in negozio le spese sono zero"), tocchi solo il mediatore.
Analogia: la torre di controllo. Gli aerei non si coordinano fra loro per atterrare: ognuno parla con la torre, e la torre decide.
Quando usarlo: - Molti oggetti dipendono l'uno dall'altro in modo incrociato. - Vuoi riusare un componente in un altro contesto senza trascinarsi le sue dipendenze. - La logica di coordinamento è complessa e vuoi vederla tutta in un punto.
Quando NON usarlo / rischi: - Il mediatore può crescere fino a diventare un "God object" che sa tutto e fa tutto. - Con due o tre componenti il mediatore è un passaggio in più senza vantaggi.
Esempio pratico: Il mediatore del checkout riceve "indirizzo cambiato", chiede a Magazzino da quale deposito spedire, calcola le spese, aggiorna il totale e abilita il pagamento. I singoli componenti restano riusabili nella pagina "modifica ordine".
from typing import Protocol
class Mediatore(Protocol):
def notifica(self, mittente: object, evento: str) -> None: ...
class Componente:
def __init__(self, mediatore: Mediatore) -> None:
self._m = mediatore
class CampoIndirizzo(Componente):
def cambia(self, valore: str) -> None:
self.valore = valore
self._m.notifica(self, "indirizzo_cambiato")
class MediatoreCheckout:
def __init__(self) -> None:
self.indirizzo = CampoIndirizzo(self)
self.spese: float = 0.0
def notifica(self, mittente: object, evento: str) -> None:
if evento == "indirizzo_cambiato":
self.spese = 0.0 if "negozio" in self.indirizzo.valore else 7.90
Pattern correlati: Observer (il mediatore spesso riceve gli eventi come observer), Facade (semplifica un sottosistema, ma in una sola direzione), Chain of Responsibility.
Memento¶
In una frase: Salva uno snapshot dello stato di un oggetto e ripristinalo quando serve, senza violare l'incapsulamento.
Problema che risolve: Il cliente compila un ordine complesso, poi preme "annulla modifiche" e vuole tornare alla versione di dieci minuti fa. Se chi salva lo snapshot deve leggere tutti i campi interni dell'ordine, ogni modifica alla classe rompe il salvataggio.
Come funziona:
flowchart LR
cliente(["🧑 Cliente"]) -->|"modifica"| ordine["📦 Ordine in modifica"]
ordine -->|"salva"| s2["📸 Snapshot 14:20"]
s1["📸 Snapshot 14:10"] --> storico[("🗄️ Storico")]
s2 --> storico
storico -->|"ripristina"| ordine
classDiagram
class Ordine {
-righe
-indirizzo
+salva() Memento
+ripristina(m: Memento)
}
class Memento {
-righe
-indirizzo
}
class StoricoOrdine {
-snapshot
+salva(ordine)
+annulla(ordine)
}
Ordine ..> Memento : crea
StoricoOrdine o-- Memento
StoricoOrdine --> Ordine
L'oggetto originale (l'ordine) crea lui stesso il memento con una copia del suo stato, e lui solo sa leggerlo. Chi tiene lo storico conserva i memento ma non guarda dentro. Per annullare, lo storico restituisce l'ultimo memento all'ordine, che ripristina i campi. I dettagli interni restano privati.
Analogia: il punto di salvataggio in un videogioco. Il gioco sa cosa salvare, tu vedi solo "slot 3, ore 14:20".
Quando usarlo: - Serve annullare, ripristinare o fare rollback dello stato di un oggetto. - Non vuoi esporre i campi interni dell'oggetto per permettere a un altro di copiarli.
Quando NON usarlo / rischi: - Gli snapshot occupano memoria: con stati grandi e salvataggi frequenti diventa un problema. - In Python l'incapsulamento è una convenzione: il pattern vale per la struttura, non per la protezione reale.
Esempio pratico: Prima di ogni modifica al carrello, il servizio Ordini salva un memento. Il pulsante "ripristina" riporta il carrello al punto scelto. Se il pagamento fallisce, lo stesso memento riporta l'ordine allo stato precedente al tentativo.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class MementoOrdine:
righe: tuple[tuple[str, int], ...]
indirizzo: str
@dataclass
class Ordine:
righe: dict[str, int] = field(default_factory=dict)
indirizzo: str = ""
def salva(self) -> MementoOrdine:
return MementoOrdine(tuple(self.righe.items()), self.indirizzo)
def ripristina(self, m: MementoOrdine) -> None:
self.righe = dict(m.righe)
self.indirizzo = m.indirizzo
class Storico:
def __init__(self) -> None:
self._snapshot: list[MementoOrdine] = []
def salva(self, o: Ordine) -> None:
self._snapshot.append(o.salva())
def annulla(self, o: Ordine) -> None:
o.ripristina(self._snapshot.pop())
Pattern correlati: Command (il comando salva un memento prima di eseguire, per l'undo), Iterator, Prototype (una copia completa dell'oggetto può fare da memento semplice).
Observer¶
In una frase: Un oggetto avvisa automaticamente tutti gli interessati quando cambia qualcosa.
Problema che risolve: Quando un ordine passa a "spedito", vanno avvisati il cliente via email, il magazzino per scaricare la giacenza, le statistiche. Se il servizio Ordini chiama direttamente ognuno, ogni nuovo interessato richiede una modifica a Ordini. E Ordini finisce per conoscere tutto il sistema.
Come funziona:
sequenceDiagram
participant O as Ordine
participant E as Notifiche Email
participant M as Magazzino
participant S as Statistiche
E->>O: iscriviti
M->>O: iscriviti
S->>O: iscriviti
Note over O: stato cambia in spedito
O->>E: aggiorna(evento)
O->>M: aggiorna(evento)
O->>S: aggiorna(evento)
L'oggetto osservato (il "subject") tiene una lista di iscritti. Chi vuole essere avvisato si iscrive con un metodo standard, ad esempio aggiorna(evento). Quando lo stato cambia, il subject scorre la lista e chiama tutti. Il subject non sa chi sono gli iscritti né cosa fanno. Aggiungere un nuovo interessato è iscriversi, senza toccare il subject.
Analogia: l'abbonamento a una rivista. L'editore non conosce i lettori per nome: ha una lista e spedisce a tutti quando esce il numero.
Fra processi diversi lo stesso concetto si chiama Publish/Subscribe: il subject diventa un topic su un broker e gli observer diventano servizi iscritti.
Quando usarlo: - Un cambiamento in un oggetto deve far reagire altri oggetti, e la lista può cambiare a runtime. - Vuoi che la sorgente dell'evento non dipenda da chi lo riceve.
Quando NON usarlo / rischi: - L'ordine di notifica non è garantito: non costruire logica che dipende da chi riceve prima. - Gli iscritti che non si disiscrivono restano in memoria (memory leak) e ricevono eventi che non vogliono più. - Con molti observer a cascata il flusso diventa difficile da seguire nel debug.
Esempio pratico: L'oggetto Ordine emette un evento a ogni cambio di stato. Notifiche manda l'email, Magazzino scarica la giacenza, le statistiche aggiornano i contatori. Nessuno dei tre è noto a Ordine.
from typing import Protocol
class Observer(Protocol):
def aggiorna(self, ordine_id: int, stato: str) -> None: ...
class Ordine:
def __init__(self, ordine_id: int) -> None:
self.ordine_id, self.stato = ordine_id, "creato"
self._iscritti: list[Observer] = []
def iscrivi(self, o: Observer) -> None:
self._iscritti.append(o)
def cambia_stato(self, nuovo: str) -> None:
self.stato = nuovo
for o in self._iscritti:
o.aggiorna(self.ordine_id, nuovo)
class NotificaEmail:
def aggiorna(self, ordine_id: int, stato: str) -> None:
print(f"email: ordine {ordine_id} ora {stato}")
Pattern correlati: Mediator (il mediatore spesso riceve eventi da observer), Command, Publish/Subscribe, Event Sourcing.
State¶
In una frase: Un oggetto cambia comportamento quando cambia il suo stato, come se cambiasse classe.
Problema che risolve: Un ordine può essere "creato", "pagato", "spedito", "consegnato", "annullato". Il metodo annulla() fa cose diverse in ogni stato, e così paga() e spedisci(). Il risultato è un if stato == ... in ogni metodo. Ogni nuovo stato tocca tutti i metodi e qualche ramo viene dimenticato.
Come funziona:
flowchart LR
ordine["📦 Ordine"] -->|"stato corrente"| creato(["🆕 Creato"])
creato -->|"paga"| pagato(["💳 Pagato"])
pagato -->|"spedisci"| spedito(["🚚 Spedito"])
spedito -->|"consegna"| cons(["✅ Consegnato"])
creato -->|"annulla"| ann(["⛔ Annullato"])
pagato -->|"rimborso"| ann
stateDiagram-v2
[*] --> Creato
Creato --> Pagato : paga
Creato --> Annullato : annulla
Pagato --> Spedito : spedisci
Pagato --> Annullato : annulla con rimborso
Spedito --> Consegnato : consegna
Consegnato --> [*]
Annullato --> [*]
Ogni stato diventa una classe con gli stessi metodi (paga, spedisci, annulla). L'ordine tiene un riferimento allo stato corrente e delega a lui ogni chiamata. Lo stato decide cosa fare e, se serve, cambia lo stato dell'ordine. Le transizioni sono esplicite e una per metodo, non sparse in tanti if. Uno stato che non supporta un'operazione alza un errore chiaro.
Analogia: il telefono. Lo stesso tasto "accensione" fa cose diverse se il telefono è spento, acceso o bloccato.
Lo stesso ciclo di vita, quando coinvolge più servizi e più transazioni, diventa una Saga orchestrazione: l'orchestratore è la macchina a stati distribuita.
Quando usarlo: - Un oggetto ha pochi stati ben definiti e il comportamento cambia molto fra uno e l'altro. - Le transizioni fra stati sono una regola di business da rendere esplicita.
Quando NON usarlo / rischi:
- Con due stati e un paio di metodi, un if è più leggibile di quattro classi.
- Le classi stato tendono a dover accedere a dati interni dell'oggetto: definisci bene cosa espone.
Esempio pratico: Ordine delega a StatoCreato, StatoPagato, StatoSpedito. Chiamare spedisci() su un ordine non pagato alza ErroreTransizione. Aggiungere lo stato "in attesa di reso" significa aggiungere una classe, non toccare le altre.
from abc import ABC, abstractmethod
class ErroreTransizione(Exception): ...
class Stato(ABC):
def paga(self, o: "Ordine") -> None:
raise ErroreTransizione("pagamento non permesso")
def spedisci(self, o: "Ordine") -> None:
raise ErroreTransizione("spedizione non permessa")
class Creato(Stato):
def paga(self, o: "Ordine") -> None:
o.stato = Pagato()
class Pagato(Stato):
def spedisci(self, o: "Ordine") -> None:
o.stato = Spedito()
class Spedito(Stato): ...
class Ordine:
def __init__(self) -> None:
self.stato: Stato = Creato()
def paga(self) -> None: self.stato.paga(self)
def spedisci(self) -> None: self.stato.spedisci(self)
Pattern correlati: Strategy (stessa struttura, ma gli stati si conoscono fra loro e si sostituiscono da soli), Saga orchestrazione, Singleton (gli stati senza dati possono essere istanze uniche).
Strategy¶
In una frase: Definisci una famiglia di algoritmi intercambiabili e scegli quale usare a runtime.
Problema che risolve: Le spese di spedizione si calcolano in modo diverso per corriere standard, espresso, ritiro in negozio. Oggi sono tre rami in un metodo. Domani arriva il corriere a peso, poi la promozione "gratis sopra 50 euro". Il metodo cresce, i test si moltiplicano, e nessuno osa toccarlo.
Come funziona:
classDiagram
class CalcoloSpedizione {
<<interface>>
+costo(ordine) float
}
class Standard {
+costo(ordine) float
}
class Espresso {
+costo(ordine) float
}
class RitiroNegozio {
+costo(ordine) float
}
class Checkout {
-strategia
+totale(ordine) float
}
CalcoloSpedizione <|.. Standard
CalcoloSpedizione <|.. Espresso
CalcoloSpedizione <|.. RitiroNegozio
Checkout o-- CalcoloSpedizione
Ogni algoritmo diventa una classe con lo stesso metodo. Il contesto (il checkout) riceve una strategia e la chiama senza sapere quale sia. Chi costruisce il checkout sceglie la strategia in base al dato (il metodo di consegna scelto dal cliente). Aggiungere un algoritmo è aggiungere una classe. Testare un algoritmo è testare una classe.
Analogia: andare in aeroporto. Puoi prendere l'autobus, il taxi o la bici. L'obiettivo è lo stesso, cambia il modo, e lo scegli in base a tempo e budget.
Quando usarlo: - Hai varianti dello stesso calcolo che cambiano in base a una scelta o a un dato. - Vuoi isolare la logica di un algoritmo dal codice che lo usa. - Vuoi poter sostituire l'algoritmo nei test con uno finto.
Quando NON usarlo / rischi:
- Con due varianti stabili, un if basta.
- Il client deve conoscere le differenze fra le strategie per scegliere: la scelta sta altrove, non sparisce.
- In Python una funzione passata come parametro è spesso una strategia sufficiente.
Esempio pratico: Il checkout chiede al cliente il tipo di consegna e costruisce la strategia giusta. Standard costa 4,90, Espresso 9,90, RitiroNegozio zero. La promozione "gratis sopra 50 euro" è una strategia che avvolge un'altra.
from typing import Protocol
from dataclasses import dataclass
@dataclass
class Ordine:
subtotale: float
peso_kg: float
class CalcoloSpedizione(Protocol):
def costo(self, ordine: Ordine) -> float: ...
class Standard:
def costo(self, ordine: Ordine) -> float:
return 4.90
class Espresso:
def costo(self, ordine: Ordine) -> float:
return 9.90 + 1.5 * ordine.peso_kg
class Checkout:
def __init__(self, strategia: CalcoloSpedizione) -> None:
self._strategia = strategia
def totale(self, ordine: Ordine) -> float:
return ordine.subtotale + self._strategia.costo(ordine)
Pattern correlati: State (stessa struttura, ma lo stato cambia da solo), Template Method (varia un algoritmo con l'ereditarietà invece che con la composizione), Decorator, Command.
Template Method¶
In una frase: Definisci lo scheletro di un algoritmo nella classe base e lascia alle sottoclassi i singoli passi.
Problema che risolve: Il servizio Notifiche manda email, SMS e push. La procedura è sempre la stessa: carica il template, compila il testo, controlla le preferenze del cliente, invia, registra l'esito. Cambia solo "compila" e "invia". Oggi le tre classi copiano l'intera procedura, e una correzione va fatta tre volte.
Come funziona:
flowchart LR
mail["✉️ NotificaEmail"] -->|"html, smtp"| base
sms["📱 NotificaSms"] -->|"160 caratteri, gateway"| base
subgraph base["📋 Notificatore, procedura fissa"]
p1["1. prepara"] --> p2["2. spedisci"] --> p3["3. registra"]
end
classDiagram
class Notificatore {
+invia(ordine) *template*
#prepara(ordine) str
#spedisci(testo)
#registra(esito)
}
class NotificaEmail {
#prepara(ordine) str
#spedisci(testo)
}
class NotificaSms {
#prepara(ordine) str
#spedisci(testo)
}
Notificatore <|-- NotificaEmail
Notificatore <|-- NotificaSms
La classe base ha un metodo "template" che chiama i passi nell'ordine giusto. Alcuni passi sono già implementati (registra l'esito), altri sono astratti e vanno scritti nella sottoclasse (prepara, spedisci). Alcuni passi possono avere un'implementazione di default vuota, un "hook", che la sottoclasse sovrascrive solo se serve. Il metodo template non si sovrascrive.
Analogia: la ricetta di una torta base. Gli step sono fissi (impasta, cuoci, decora), ma ogni pasticcere cambia la farcitura.
Quando usarlo: - Più classi condividono la stessa procedura e cambiano solo in pochi passi. - Vuoi che le sottoclassi estendano solo punti precisi e non l'intera procedura.
Quando NON usarlo / rischi: - Si basa sull'ereditarietà: se i passi variabili crescono, la gerarchia diventa rigida. In quel caso passa a Strategy. - Le sottoclassi dipendono dai dettagli della classe base: una modifica nell'ordine dei passi le rompe tutte.
Esempio pratico: Notificatore.invia() carica le preferenze, chiama prepara(), poi spedisci(), poi registra l'esito su un log. NotificaEmail e NotificaSms scrivono solo i due passi che differiscono.
from abc import ABC, abstractmethod
class Notificatore(ABC):
def invia(self, ordine_id: int, stato: str) -> None:
testo = self.prepara(ordine_id, stato)
self.spedisci(testo)
self.registra(ordine_id)
@abstractmethod
def prepara(self, ordine_id: int, stato: str) -> str: ...
@abstractmethod
def spedisci(self, testo: str) -> None: ...
def registra(self, ordine_id: int) -> None:
print(f"notifica inviata per ordine {ordine_id}")
class NotificaSms(Notificatore):
def prepara(self, ordine_id: int, stato: str) -> str:
return f"Ordine {ordine_id}: {stato}"[:160]
def spedisci(self, testo: str) -> None:
print("sms:", testo)
Pattern correlati: Strategy (composizione al posto dell'ereditarietà), Factory Method (è un caso particolare di Template Method), Abstract Factory.
Visitor¶
In una frase: Aggiungi una nuova operazione a una famiglia di classi senza modificare le classi.
Problema che risolve: Il Catalogo ha prodotti fisici, digitali e abbonamenti. Devi calcolare l'IVA, esportare in XML, stimare il peso per la spedizione. Ogni nuova operazione aggiunge un metodo a ogni classe prodotto, e le classi si riempiono di logica che non è loro (l'export non è compito del prodotto).
Come funziona:
flowchart LR
iva["🧾 Visitor CalcoloIva"] -->|"22%"| fis
iva -->|"4%"| dig
peso["⚖️ Visitor StimaPeso"] -->|"kg"| fis
subgraph cat["🏷️ Catalogo"]
fis["📦 Prodotto fisico"]
dig["💾 Prodotto digitale"]
abb["🔁 Abbonamento"]
end
classDiagram
class Prodotto {
<<interface>>
+accetta(v: Visitor)
}
class ProdottoFisico {
+accetta(v: Visitor)
}
class ProdottoDigitale {
+accetta(v: Visitor)
}
class Visitor {
<<interface>>
+visita_fisico(p)
+visita_digitale(p)
}
class CalcoloIva {
+visita_fisico(p)
+visita_digitale(p)
}
Prodotto <|.. ProdottoFisico
Prodotto <|.. ProdottoDigitale
Visitor <|.. CalcoloIva
ProdottoFisico ..> Visitor : accetta
Ogni classe della famiglia ha un solo metodo accetta(visitor) che chiama il metodo del visitor giusto per il suo tipo (visita_fisico, visita_digitale). Questo "doppio dispatch" serve perché il visitor sappia il tipo concreto senza isinstance. Ogni operazione diventa una classe visitor con un metodo per tipo. Le classi prodotto non cambiano più.
Analogia: l'agente assicurativo che passa di porta in porta. In una casa vende la polizza casa, in una banca la polizza furto. Lui conosce ogni tipo di edificio; l'edificio deve solo aprirgli.
Quando usarlo: - La famiglia di classi è stabile e le operazioni su di essa cambiano spesso. - Vuoi tenere operazioni "esterne" (export, report, calcoli) fuori dalle classi di dominio. - Devi percorrere una struttura complessa (albero, Composite) applicando un'operazione diversa per tipo di nodo.
Quando NON usarlo / rischi:
- Se aggiungi spesso nuove classi alla famiglia, ogni aggiunta tocca tutti i visitor.
- Il visitor spesso ha bisogno di accedere ai dati interni degli elementi: finisce per esporli.
- In Python functools.singledispatch copre molti casi semplici con meno struttura.
Esempio pratico: CalcoloIva applica il 22 per cento ai prodotti fisici e il 4 per cento ai libri digitali. StimaPeso conta solo i fisici. Nessuno dei due vive dentro le classi prodotto.
from typing import Protocol
from dataclasses import dataclass
class Visitor(Protocol):
def visita_fisico(self, p: "ProdottoFisico") -> float: ...
def visita_digitale(self, p: "ProdottoDigitale") -> float: ...
@dataclass
class ProdottoFisico:
prezzo: float
peso_kg: float
def accetta(self, v: Visitor) -> float:
return v.visita_fisico(self)
@dataclass
class ProdottoDigitale:
prezzo: float
def accetta(self, v: Visitor) -> float:
return v.visita_digitale(self)
class CalcoloIva:
def visita_fisico(self, p: ProdottoFisico) -> float:
return p.prezzo * 0.22
def visita_digitale(self, p: ProdottoDigitale) -> float:
return p.prezzo * 0.04
Pattern correlati: Composite (il visitor percorre l'albero), Iterator (scorre gli elementi su cui applicare il visitor), Command.
Strategy vs State vs Template Method¶
| Pattern | Cosa varia | Chi sceglie la variante | Meccanismo |
|---|---|---|---|
| Strategy | Un algoritmo intero | Il client, dall'esterno | Composizione: il contesto riceve l'oggetto strategia |
| State | Il comportamento in base allo stato | L'oggetto stesso, le transizioni sono interne | Composizione: gli stati si sostituiscono a vicenda |
| Template Method | Alcuni passi di una procedura fissa | È fissato al momento della scrittura della sottoclasse | Ereditarietà: la sottoclasse sovrascrive i passi |
Observer vs Mediator¶
| Pattern | Direzione della comunicazione | Chi conosce chi | Usalo quando |
|---|---|---|---|
| Observer | Uno a molti, dal subject agli iscritti | Il subject ha una lista anonima di iscritti | Vuoi avvisare chiunque sia interessato, senza coordinare le reazioni |
| Mediator | Molti a molti, tutto passa per il centro | Ogni componente conosce solo il mediatore, che conosce tutti | Le reazioni vanno coordinate in un ordine preciso e in un punto solo |
Come scegliere¶
flowchart TD
q1{"Devi aggiungere operazioni a una gerarchia stabile?"}
q1 -- si --> visitor["Visitor"]
q1 -- no --> q2{"Devi scorrere una collezione nascondendo la struttura?"}
q2 -- si --> iterator["Iterator"]
q2 -- no --> q3{"Serve annullare o accodare azioni?"}
q3 -- si --> q4{"Puoi calcolare l'inverso dell'azione?"}
q4 -- si --> command["Command"]
q4 -- no --> memento["Memento"]
q3 -- no --> q5{"Un oggetto deve avvisare altri quando cambia?"}
q5 -- si --> q6{"Le reazioni vanno coordinate fra loro?"}
q6 -- si --> mediator["Mediator"]
q6 -- no --> observer["Observer"]
q5 -- no --> q7{"Il comportamento cambia in base a qualcosa?"}
q7 -- "allo stato interno" --> state["State"]
q7 -- "a una scelta esterna" --> strategy["Strategy"]
q7 -- "a pochi passi di una procedura fissa" --> template["Template Method"]
q7 -- no --> chain["Chain of Responsibility"]