Design pattern strutturali¶
I pattern strutturali spiegano come comporre oggetti e classi in strutture più grandi, mantenendo queste strutture flessibili ed efficienti. Il tema comune è l'interposizione: un oggetto si mette "in mezzo" fra chi chiama e chi risponde per adattare, estendere, semplificare o controllare l'accesso.
flowchart LR
cliente(["🧑 Chi chiama"])
subgraph mezzo["🧩 Un oggetto in mezzo"]
adapter["🔌 Adapter: traduce"]
decorator["🧥 Decorator: aggiunge"]
proxy["🛂 Proxy: controlla"]
facade["☎️ Facade: semplifica"]
end
subgraph forma["🌳 Una forma per molti oggetti"]
composite["🌲 Composite: albero"]
bridge["🌉 Bridge: due dimensioni"]
flyweight["🪶 Flyweight: stato condiviso"]
end
reale["⚙️ Oggetto reale"]
cliente --> mezzo --> reale
| Pattern | Problema che risolve | Usalo quando |
|---|---|---|
| Adapter | Due interfacce incompatibili devono collaborare | Integri una libreria o un'API esterna che non controlli |
| Bridge | Due dimensioni di variazione fanno esplodere le sottoclassi | Astrazione e implementazione devono evolvere in modo indipendente |
| Composite | Oggetti singoli e gruppi vanno trattati allo stesso modo | Hai una gerarchia ad albero di parti e contenitori |
| Decorator | Aggiungere comportamenti senza toccare la classe originale | Vuoi combinare funzionalità opzionali a runtime |
| Facade | Un sottosistema complesso espone troppe classi | Serve un punto di ingresso semplice per i casi comuni |
| Flyweight | Troppi oggetti simili consumano troppa memoria | Milioni di istanze condividono gran parte dello stato |
| Proxy | Controllare o posticipare l'accesso a un oggetto | Serve cache, lazy loading, controllo accessi o logging |
Cosa sono¶
Un pattern strutturale descrive come mettere insieme classi e oggetti in strutture più grandi senza irrigidirle. Invece di ereditare, questi pattern preferiscono comporre: un oggetto ne avvolge un altro e aggiunge, traduce o filtra il comportamento. Il risultato è codice che cambia aggiungendo pezzi, non modificando quelli esistenti.
Per approfondire con analogie e codice in altri linguaggi: refactoring.guru/design-patterns/structural-patterns.
Adapter¶
In una frase: Un traduttore che fa collaborare due interfacce incompatibili.
Problema che risolve: Il servizio Notifiche spedisce con un corriere che espone crea_spedizione(indirizzo, peso_grammi). Arriva un secondo corriere con una libreria che vuole ship(dict) e il peso in chilogrammi. Non puoi modificare la libreria e non vuoi riempire il codice di if corriere == ....
Come funziona:
flowchart LR
notifiche["✉️ Servizio Notifiche"]
adapter["🔌 AdapterExpress"]
lib["📦 LibreriaExpress"]
corriere>"🚚 Corriere Express"]
notifiche -->|"crea_spedizione(indirizzo, grammi)"| adapter
adapter -->|"ship(payload) in kg"| lib
lib --> corriere
classDiagram
class Corriere {
<<interface>>
+crea_spedizione(indirizzo, peso_grammi) str
}
class CorriereNazionale {
+crea_spedizione(indirizzo, peso_grammi) str
}
class LibreriaExpress {
+ship(payload) dict
}
class AdapterExpress {
-express LibreriaExpress
+crea_spedizione(indirizzo, peso_grammi) str
}
Corriere <|.. CorriereNazionale
Corriere <|.. AdapterExpress
AdapterExpress o-- LibreriaExpress
class ServizioSpedizioni {
-corriere Corriere
}
ServizioSpedizioni --> Corriere
Corriereè l'interfaccia che il tuo codice si aspetta.LibreriaExpressè la classe esterna con un'interfaccia diversa.AdapterExpressimplementaCorrieree al suo interno tiene un'istanza diLibreriaExpress.- Ogni chiamata a
crea_spedizioneviene tradotta in una chiamata aship, convertendo i dati. ServizioSpedizionilavora solo conCorrieree non sa che esiste un adapter.
Analogia: l'adattatore di corrente per la presa estera. La spina non cambia, la rete elettrica non cambia, l'adattatore fa da ponte.
Quando usarlo: - Devi usare una classe esistente, ma la sua interfaccia non coincide con il resto del codice. - Vuoi riutilizzare più classi di terze parti dietro un'unica interfaccia. - Vuoi isolare il codice da una libreria che potrebbe cambiare.
Quando NON usarlo / rischi: - Se controlli entrambe le classi, cambia direttamente l'interfaccia: l'adapter è un livello in più. - Troppi adapter annidati rendono difficile capire dove finiscono i dati. - Non usarlo per nascondere differenze semantiche profonde (esempio: una API sincrona e una asincrona).
Esempio pratico:
from typing import Protocol
class Corriere(Protocol):
def crea_spedizione(self, indirizzo: str, peso_grammi: int) -> str: ...
class LibreriaExpress: # classe esterna, non modificabile
def ship(self, payload: dict) -> dict:
return {"tracking": f"EXP-{payload['kg']}"}
class AdapterExpress:
def __init__(self, express: LibreriaExpress) -> None:
self._express = express
def crea_spedizione(self, indirizzo: str, peso_grammi: int) -> str:
risposta = self._express.ship({"to": indirizzo, "kg": peso_grammi / 1000})
return risposta["tracking"]
def spedisci(corriere: Corriere) -> str:
return corriere.crea_spedizione("Via Roma 1, Milano", 1500)
print(spedisci(AdapterExpress(LibreriaExpress())))
Pattern correlati: Bridge (si progetta prima, l'Adapter si aggiunge dopo), Decorator (stessa interfaccia, aggiunge comportamento), Proxy (stessa interfaccia, controlla l'accesso), Facade (semplifica un intero sottosistema), Anti-Corruption Layer.
Bridge¶
In una frase: Separa l'astrazione dall'implementazione così che le due possano evolvere da sole.
Problema che risolve: Il servizio Notifiche deve inviare messaggi di tipo diverso (conferma ordine, spedizione partita, reso approvato) su canali diversi (email, SMS, push). Con l'ereditarietà finisci con ConfermaOrdineEmail, ConfermaOrdineSms, SpedizionePush... Ogni nuovo tipo o canale moltiplica le classi.
Come funziona:
flowchart LR
subgraph cosa["💬 Cosa dire"]
conferma["📄 Conferma ordine"]
spedita["📄 Spedizione partita"]
end
ponte{{"🌉 Ponte"}}
subgraph come["📡 Come inviare"]
email["✉️ Email"]
sms["📱 SMS"]
push["🔔 Push"]
end
cliente(["🧑 Cliente"])
cosa --> ponte --> come --> cliente
classDiagram
class Notifica {
-canale Canale
+invia(destinatario)
}
class ConfermaOrdine {
+invia(destinatario)
}
class SpedizionePartita {
+invia(destinatario)
}
class Canale {
<<interface>>
+consegna(destinatario, testo)
}
class CanaleEmail {
+consegna(destinatario, testo)
}
class CanaleSms {
+consegna(destinatario, testo)
}
Notifica <|-- ConfermaOrdine
Notifica <|-- SpedizionePartita
Notifica o-- Canale
Canale <|.. CanaleEmail
Canale <|.. CanaleSms
Notificaè l'astrazione: sa cosa dire, non come trasmetterlo.Canaleè l'implementazione: sa trasmettere, non cosa dire.- L'astrazione tiene un riferimento all'implementazione (il "ponte").
- Aggiungere un tipo di notifica crea una sottoclasse di
Notifica; aggiungere un canale crea una sottoclasse diCanale. - Le combinazioni nascono a runtime:
ConfermaOrdine(CanaleSms()).
Analogia: telecomando e televisore. Un telecomando (astrazione) funziona con qualunque marca di TV (implementazione), e puoi migliorare l'uno senza toccare l'altro.
Quando usarlo: - Una classe varia lungo due o più dimensioni indipendenti. - Vuoi cambiare l'implementazione a runtime. - Vuoi che team diversi lavorino su astrazione e implementazione senza pestarsi i piedi.
Quando NON usarlo / rischi: - Se c'è una sola dimensione di variazione, basta una semplice interfaccia. - Introduce indirezione: con poche classi il codice diventa più difficile da seguire, non più semplice. - Decidere "cosa è astrazione e cosa implementazione" richiede una progettazione iniziale.
Esempio pratico:
from abc import ABC, abstractmethod
from typing import Protocol
class Canale(Protocol):
def consegna(self, destinatario: str, testo: str) -> None: ...
class CanaleEmail:
def consegna(self, destinatario: str, testo: str) -> None:
print(f"EMAIL a {destinatario}: {testo}")
class Notifica(ABC):
def __init__(self, canale: Canale) -> None:
self._canale = canale
@abstractmethod
def invia(self, destinatario: str) -> None: ...
class ConfermaOrdine(Notifica):
def __init__(self, canale: Canale, ordine_id: str) -> None:
super().__init__(canale)
self._ordine_id = ordine_id
def invia(self, destinatario: str) -> None:
self._canale.consegna(destinatario, f"Ordine {self._ordine_id} confermato")
ConfermaOrdine(CanaleEmail(), "A-42").invia("anna@example.com")
Pattern correlati: Adapter (ripara interfacce esistenti, il Bridge le progetta), Abstract Factory (può creare le implementazioni), Strategy (struttura simile, ma lo Strategy cambia un algoritmo).
Composite¶
In una frase: Tratta un oggetto singolo e un gruppo di oggetti con la stessa interfaccia.
Problema che risolve: Il Catalogo vende prodotti singoli e bundle ("kit scrivania": monitor, tastiera, supporto). Un bundle può contenere altri bundle. Calcolare il prezzo o il peso del carrello richiede di distinguere continuamente "è un prodotto o un bundle?" e di gestire la ricorsione a mano.
Come funziona:
flowchart TD
carrello(["🛒 Carrello"])
kit["🎁 Bundle Kit scrivania"]
monitor["🖥️ Monitor 199"]
tastiera["⌨️ Tastiera 49"]
acc["🎁 Bundle Accessori"]
mouse["🖱️ Mouse"]
tapp["🟫 Tappetino"]
carrello -->|"prezzo()"| kit
kit --> monitor
kit --> tastiera
kit --> acc
acc --> mouse
acc --> tapp
classDiagram
class Articolo {
<<interface>>
+prezzo() Decimal
}
class Prodotto {
-nome str
-costo Decimal
+prezzo() Decimal
}
class Bundle {
-figli List~Articolo~
-sconto Decimal
+aggiungi(articolo)
+prezzo() Decimal
}
Articolo <|.. Prodotto
Articolo <|.. Bundle
Bundle o-- Articolo : contiene
Articoloè l'interfaccia comune: esponeprezzo().Prodottoè la foglia: restituisce il proprio costo.Bundleè il contenitore: tiene una lista diArticoloe calcola il prezzo sommando i figli.- Siccome un
Bundleè anche unArticolo, può contenere altri bundle: l'albero cresce senza codice in più. - Il carrello chiama
prezzo()sulla radice e la ricorsione avviene da sola.
Analogia: un esercito. Un ordine dato al generale scende a divisioni, brigate, plotoni, fino al singolo soldato: ogni livello obbedisce alla stessa interfaccia "esegui ordine".
Quando usarlo: - Il dominio ha una struttura ad albero (bundle, categorie, menu, componenti UI). - Il client deve trattare foglie e contenitori allo stesso modo.
Quando NON usarlo / rischi:
- Se la struttura è piatta, una semplice lista basta.
- L'interfaccia comune rischia di essere troppo generica: aggiungi() su una foglia non ha senso.
- Operazioni che dipendono dal tipo (sconti solo sui bundle) complicano il codice.
Esempio pratico:
from decimal import Decimal
from typing import Protocol
class Articolo(Protocol):
def prezzo(self) -> Decimal: ...
class Prodotto:
def __init__(self, nome: str, costo: Decimal) -> None:
self.nome, self._costo = nome, costo
def prezzo(self) -> Decimal:
return self._costo
class Bundle:
def __init__(self, sconto: Decimal = Decimal("0")) -> None:
self._figli: list[Articolo] = []
self._sconto = sconto
def aggiungi(self, articolo: Articolo) -> None:
self._figli.append(articolo)
def prezzo(self) -> Decimal:
totale = sum((a.prezzo() for a in self._figli), Decimal("0"))
return totale - self._sconto
kit = Bundle(sconto=Decimal("10"))
kit.aggiungi(Prodotto("Monitor", Decimal("199")))
kit.aggiungi(Prodotto("Tastiera", Decimal("49")))
print(kit.prezzo()) # 238
Pattern correlati: Decorator (struttura simile, ma con un solo figlio), Iterator (per attraversare l'albero), Visitor (per operazioni sull'intero albero), Builder (per costruire alberi complessi).
Decorator¶
In una frase: Avvolge un oggetto in un altro con la stessa interfaccia per aggiungergli comportamento.
Problema che risolve: Il client HTTP di Pagamenti deve fare logging, poi riprovare in caso di errore, poi misurare i tempi. Metterli nella classe la gonfia e ogni combinazione ("solo retry", "retry e logging") richiede una sottoclasse. Serve un modo per aggiungere i comportamenti uno alla volta, nell'ordine che vuoi.
Come funziona:
classDiagram
class ClientPagamenti {
<<interface>>
+addebita(importo) str
}
class ClientHttp {
+addebita(importo) str
}
class DecoratorBase {
-interno ClientPagamenti
+addebita(importo) str
}
class ConLogging {
+addebita(importo) str
}
class ConRetry {
-tentativi int
+addebita(importo) str
}
ClientPagamenti <|.. ClientHttp
ClientPagamenti <|.. DecoratorBase
DecoratorBase o-- ClientPagamenti : avvolge
DecoratorBase <|-- ConLogging
DecoratorBase <|-- ConRetry
ClientPagamentiè l'interfaccia che tutti rispettano, compresi i decoratori.ClientHttpè l'oggetto concreto che fa il lavoro vero.- Ogni decoratore tiene un riferimento a un altro
ClientPagamenti(l'"interno"). - Il decoratore fa qualcosa prima o dopo, poi delega all'interno.
- Impilando i decoratori ottieni una catena:
ConLogging(ConRetry(ClientHttp())). Il chiamante vede sempre unClientPagamenti.
Analogia: vestirsi a strati. Maglia, felpa, giacca: ogni strato aggiunge qualcosa e li puoi togliere o riordinare, ma sotto resti sempre tu.
Nei microservizi questo pattern è ovunque: un Retry e un Circuit Breaker sono decoratori di un client. Avvolgono la chiamata remota, aggiungono la logica di resilienza e lasciano l'interfaccia intatta. Un service mesh fa lo stesso, ma fuori dal processo.
Quando usarlo:
- Vuoi aggiungere responsabilità a singoli oggetti a runtime, senza toccarne la classe.
- Le funzionalità opzionali si combinano in molti modi (logging, retry, cache, metriche).
- L'ereditarietà è bloccata (classe final) o produrrebbe troppe sottoclassi.
Quando NON usarlo / rischi:
- Una catena lunga è difficile da ispezionare: "quale strato ha fallito?".
- L'ordine conta: Retry(CircuitBreaker(x)) e CircuitBreaker(Retry(x)) si comportano in modo diverso.
- Se il comportamento è sempre lo stesso per tutti, mettilo nella classe e basta.
Esempio pratico:
from typing import Protocol
class ClientPagamenti(Protocol):
def addebita(self, importo: int) -> str: ...
class ClientHttp:
def addebita(self, importo: int) -> str:
return f"ok:{importo}"
class ConRetry:
def __init__(self, interno: ClientPagamenti, tentativi: int = 3) -> None:
self._interno, self._tentativi = interno, tentativi
def addebita(self, importo: int) -> str:
for n in range(1, self._tentativi + 1):
try:
return self._interno.addebita(importo)
except ConnectionError:
if n == self._tentativi:
raise
raise RuntimeError("non raggiungibile")
class ConLogging:
def __init__(self, interno: ClientPagamenti) -> None:
self._interno = interno
def addebita(self, importo: int) -> str:
print(f"addebito {importo}")
return self._interno.addebita(importo)
client: ClientPagamenti = ConLogging(ConRetry(ClientHttp()))
print(client.addebita(1999))
Pattern correlati: Proxy (stessa forma, ma controlla l'accesso invece di aggiungere), Adapter (cambia l'interfaccia, il Decorator la mantiene), Composite (un Decorator è un Composite con un solo figlio), Chain of Responsibility, Retry, Circuit Breaker.
Facade¶
In una frase: Un'unica interfaccia semplice davanti a un sottosistema complesso.
Problema che risolve: Per completare un checkout il frontend deve: riservare la scorta in Magazzino, addebitare in Pagamenti, creare l'ordine in Ordini, inviare la conferma da Notifiche. Nell'ordine giusto, con i rollback giusti. Ogni client che ripete questa sequenza duplica logica e sbaglia prima o poi.
Come funziona:
classDiagram
class CheckoutFacade {
+completa(carrello, cliente) str
}
class Magazzino {
+riserva(articoli)
}
class Pagamenti {
+addebita(cliente, importo)
}
class Ordini {
+crea(carrello) str
}
class Notifiche {
+conferma(cliente, ordine_id)
}
class Frontend
Frontend --> CheckoutFacade
CheckoutFacade --> Magazzino
CheckoutFacade --> Pagamenti
CheckoutFacade --> Ordini
CheckoutFacade --> Notifiche
CheckoutFacadeespone un solo metodo,completa().- Dentro, chiama i quattro servizi nell'ordine corretto e gestisce gli errori.
- Il
Frontendconosce solo la facade: non sa quanti servizi ci sono dietro. - I servizi restano accessibili direttamente per i casi avanzati: la facade non li nasconde, li semplifica.
Analogia: l'operatore del call center. Tu chiami un numero, lui parla con fatturazione, tecnici e logistica per te.
Quando usarlo: - Vuoi un punto di ingresso semplice per il caso d'uso più comune di un sottosistema. - Vuoi disaccoppiare i client dalle classi interne, così puoi cambiarle senza rompere nulla. - Vuoi organizzare un sistema a livelli: una facade per ogni livello.
Quando NON usarlo / rischi: - La facade può diventare un "god object" che sa tutto e fa tutto. - Se nasconde troppo, i client non riescono a fare i casi particolari e la aggirano. - Non sostituisce una buona progettazione del sottosistema: se è un disastro dentro, resta un disastro.
Esempio pratico:
from dataclasses import dataclass
@dataclass
class Carrello:
articoli: list[str]
totale: int
class CheckoutFacade:
def __init__(self, magazzino, pagamenti, ordini, notifiche) -> None:
self._magazzino, self._pagamenti = magazzino, pagamenti
self._ordini, self._notifiche = ordini, notifiche
def completa(self, carrello: Carrello, cliente: str) -> str:
self._magazzino.riserva(carrello.articoli)
try:
self._pagamenti.addebita(cliente, carrello.totale)
except Exception:
self._magazzino.rilascia(carrello.articoli)
raise
ordine_id = self._ordini.crea(carrello)
self._notifiche.conferma(cliente, ordine_id)
return ordine_id
Pattern correlati: Adapter (adatta una classe, la Facade semplifica molte), Mediator (coordina componenti che si conoscono a vicenda), Singleton (spesso la facade è una sola), API Gateway (la stessa idea a livello di rete), Saga (quando il checkout è distribuito).
Flyweight¶
In una frase: Condivide la parte comune dello stato fra molti oggetti per risparmiare memoria.
Problema che risolve: Il Catalogo tiene in memoria due milioni di varianti di prodotto. Ogni variante porta con sé descrizione, immagini e scheda tecnica del modello base, identiche per migliaia di varianti. La memoria esplode mentre l'unica cosa che cambia davvero è taglia, colore e giacenza.
Come funziona:
flowchart LR
fabbrica["🏭 FabbricaModelli"]
modello["👕 Modello T-shirt: descrizione, immagini"]
v1["🏷️ S nero, giacenza 12"]
v2["🏷️ M bianco, giacenza 3"]
v3["🏷️ L nero, giacenza 0"]
v4["🏷️ ... x 2 milioni"]
fabbrica -->|"crea una volta"| modello
v1 --> modello
v2 --> modello
v3 --> modello
v4 --> modello
classDiagram
class ModelloProdotto {
-nome str
-descrizione str
-immagini List~str~
}
class Variante {
-modello ModelloProdotto
-taglia str
-colore str
-giacenza int
}
class FabbricaModelli {
-cache dict
+ottieni(nome) ModelloProdotto
}
Variante --> ModelloProdotto : stato condiviso
FabbricaModelli --> ModelloProdotto : crea o riusa
Variante ..> FabbricaModelli
ModelloProdottoè il flyweight: contiene lo stato intrinseco, uguale per tutti e immutabile.Variantecontiene lo stato estrinseco: taglia, colore, giacenza, diverso per ogni istanza.FabbricaModelligarantisce che ogni modello esista una volta sola: se c'è in cache lo restituisce, altrimenti lo crea.- Due milioni di varianti puntano a poche migliaia di modelli: la memoria scende di ordini di grandezza.
Analogia: in un videogioco con una foresta di un milione di alberi, la texture e il modello 3D sono caricati una volta; ogni albero tiene solo posizione e dimensione.
Quando usarlo: - Hai un numero enorme di oggetti e la memoria è un problema reale e misurato. - Gran parte dello stato può essere estratto, condiviso e reso immutabile.
Quando NON usarlo / rischi: - Ottimizzazione prematura: se la memoria non è un problema, il codice diventa solo più complicato. - Lo stato condiviso deve essere immutabile, altrimenti una modifica si propaga a tutti. - Calcolare lo stato estrinseco a ogni chiamata può costare CPU.
Esempio pratico:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class ModelloProdotto: # stato intrinseco, condiviso e immutabile
nome: str
descrizione: str
immagini: tuple[str, ...]
class FabbricaModelli:
def __init__(self) -> None:
self._cache: dict[str, ModelloProdotto] = {}
def ottieni(self, nome: str, descrizione: str, immagini: tuple[str, ...]) -> ModelloProdotto:
if nome not in self._cache:
self._cache[nome] = ModelloProdotto(nome, descrizione, immagini)
return self._cache[nome]
@dataclass
class Variante: # stato estrinseco
modello: ModelloProdotto
taglia: str
colore: str
giacenza: int = 0
fabbrica = FabbricaModelli()
base = fabbrica.ottieni("T-shirt", "Cotone 100%", ("front.jpg", "back.jpg"))
varianti = [Variante(base, t, c) for t in "SML" for c in ("nero", "bianco")]
Pattern correlati: Composite (le foglie condivise di un albero possono essere flyweight), Singleton (un flyweight è "un singleton per chiave"), Factory Method (la fabbrica che gestisce la cache), Proxy (per cache di oggetti remoti, non di memoria).
Proxy¶
In una frase: Un sostituto con la stessa interfaccia che controlla l'accesso all'oggetto reale.
Problema che risolve: Ordini interroga il Catalogo via rete per ogni riga di ogni carrello. Le schede prodotto cambiano una volta al giorno, ma la chiamata parte sempre. Vuoi una cache, ma senza che Ordini debba sapere che esiste e senza modificare il client del catalogo.
Come funziona:
flowchart LR
ordini["📦 Servizio Ordini"]
proxy["🛂 CatalogoConCache"]
cache[("🗄️ Cache locale, TTL 1 ora")]
reale["📚 CatalogoRemoto"]
ordini -->|"scheda(MON-27)"| proxy
proxy -->|"se valido, restituisci"| cache
proxy -.->|"altrimenti chiama e salva"| reale
classDiagram
class Catalogo {
<<interface>>
+scheda(sku) dict
}
class CatalogoRemoto {
+scheda(sku) dict
}
class CatalogoConCache {
-reale Catalogo
-cache dict
-ttl int
+scheda(sku) dict
}
class ServizioOrdini {
-catalogo Catalogo
}
Catalogo <|.. CatalogoRemoto
Catalogo <|.. CatalogoConCache
CatalogoConCache o-- Catalogo : delega se serve
ServizioOrdini --> Catalogo
Catalogoè l'interfaccia comune a oggetto reale e proxy.CatalogoRemotofa la chiamata HTTP vera.CatalogoConCachericeve la richiesta prima: se ha il dato valido lo restituisce, altrimenti delega aCatalogoRemotoe salva.ServizioOrdinitiene unCatalogoe non distingue fra proxy e reale.- Lo stesso schema vale per altri tipi di proxy: lazy loading (crea l'oggetto pesante alla prima chiamata), protezione (verifica i permessi), logging (registra le chiamate).
Analogia: la carta di credito è un proxy del contante. Il negozio la accetta come fosse denaro, ma la banca controlla ogni transazione.
Quando usarlo: - Cache di risultati costosi o remoti. - Inizializzazione pigra di oggetti pesanti. - Controllo accessi, logging o conteggio dei riferimenti senza toccare l'oggetto reale.
Quando NON usarlo / rischi: - Una cache nel proxy introduce il problema dell'invalidazione: dati vecchi silenziosi. - Aggiunge latenza e un punto di fallimento in più se il proxy è remoto. - Nasconde al chiamante cosa succede davvero: il debug diventa più difficile.
Esempio pratico:
import time
from typing import Protocol
class Catalogo(Protocol):
def scheda(self, sku: str) -> dict: ...
class CatalogoRemoto:
def scheda(self, sku: str) -> dict:
return {"sku": sku, "nome": "Monitor 27"} # qui ci sarebbe una chiamata HTTP
class CatalogoConCache:
def __init__(self, reale: Catalogo, ttl_secondi: int = 3600) -> None:
self._reale, self._ttl = reale, ttl_secondi
self._cache: dict[str, tuple[float, dict]] = {}
def scheda(self, sku: str) -> dict:
salvato = self._cache.get(sku)
if salvato and time.monotonic() - salvato[0] < self._ttl:
return salvato[1]
dato = self._reale.scheda(sku)
self._cache[sku] = (time.monotonic(), dato)
return dato
catalogo: Catalogo = CatalogoConCache(CatalogoRemoto())
print(catalogo.scheda("MON-27"))
Pattern correlati: Decorator (stessa forma, ma il Decorator aggiunge comportamento e il Proxy controlla l'accesso), Adapter (cambia l'interfaccia, il Proxy la mantiene), Facade (semplifica, non sostituisce), Circuit Breaker (un proxy di protezione verso un servizio remoto), Sidecar (un proxy fuori dal processo).
Adapter vs Decorator vs Proxy vs Facade¶
Hanno tutti la stessa forma, un oggetto che ne avvolge un altro, ma intento diverso.
| Pattern | Interfaccia esposta | Intento | Frase chiave |
|---|---|---|---|
| Adapter | Diversa da quella avvolta | Far collaborare interfacce incompatibili | "Traduce" |
| Decorator | Uguale a quella avvolta | Aggiungere comportamento, impilabile | "Aggiunge" |
| Proxy | Uguale a quella avvolta | Controllare l'accesso all'oggetto reale | "Controlla" |
| Facade | Nuova e più semplice | Semplificare un intero sottosistema | "Semplifica" |
Come scegliere¶
flowchart TD
q1{"L'interfaccia che hai è diversa da quella che ti serve?"}
q1 -- sì --> q1b{"Avvolgi una classe o un intero sottosistema?"}
q1b -- una classe --> adapter["Adapter"]
q1b -- un sottosistema --> facade["Facade"]
q1 -- no --> q2{"Vuoi aggiungere comportamento senza cambiare l'interfaccia?"}
q2 -- sì --> q2b{"Aggiungi funzionalità o controlli l'accesso?"}
q2b -- aggiungi --> decorator["Decorator"]
q2b -- controllo o cache --> proxy["Proxy"]
q2 -- no --> q3{"Hai una struttura ad albero di parti e contenitori?"}
q3 -- sì --> composite["Composite"]
q3 -- no --> q4{"Una classe varia lungo due dimensioni indipendenti?"}
q4 -- sì --> bridge["Bridge"]
q4 -- no --> q5{"Troppi oggetti simili consumano memoria?"}
q5 -- sì --> flyweight["Flyweight"]
q5 -- no --> nessuno["Probabilmente non serve un pattern strutturale"]