Vai al contenuto

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:

Illustrazione: l'abbonamento alla rivista, l'ordine spedisce la copia a chi è abbonato

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:

Illustrazione: il navigatore, stessa destinazione e tre percorsi intercambiabili

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"]