Pattern di progettazione strutturale: Facade, Flyweight e Proxy
I pattern di progettazione strutturale ti aiutano a organizzare classi e oggetti.
Oggi esamineremo gli ultimi tre pattern per completare il tuo toolkit.
1. Il pattern Facade Facade semplifica i sistemi complessi. Fornisce un'unica interfaccia semplice a un gruppo di classi disordinate.
Pensa a un cinema. Per guardare un film, devi abbassare le luci, accendere il proiettore e aprire le tende. Invece di chiamare cinque sistemi diversi, chiami un unico metodo: theater.watch_movie().
Usalo quando:
- Vuoi semplificare un sottosistema complesso.
- Hai bisogno di un unico punto di ingresso per una grande API.
- Vuoi disaccoppiare i client dalla logica interna.
2. Il pattern Flyweight Flyweight risparmia memoria. Funziona quando hai migliaia di oggetti simili.
Invece di memorizzare ogni dettaglio in ogni oggetto, dividi i dati. Mantieni i dati condivisi e immutabili (stato intrinseco) in un unico posto. Mantieni separati i dati unici (stato estrinseco).
Usalo quando:
- L'uso della memoria è un problema reale.
- Gestisci milioni di oggetti simili, come i caratteri in un editor di testo o le particelle in un gioco.
- Vuoi utilizzare l'object pooling per migliorare le prestazioni.
3. Il pattern Proxy Proxy agisce come sostituto di un altro oggetto. Si posiziona tra il client e l'oggetto reale per controllarne l'accesso.
Un proxy può:
- Lazy load: caricare immagini pesanti solo quando un utente ci clicca sopra.
- Controllo dell'accesso: verificare se un utente ha il permesso di eliminare un database.
- Registrazione dell'attività: tracciare chi utilizza un servizio specifico.
- Caching dei risultati: restituire dati salvati invece di eseguire logiche costose.
Usalo quando:
- Hai bisogno di ritardare operazioni costose.
- Devi proteggere un servizio sensibile.
- Vuoi aggiungere logging o sicurezza senza modificare la classe originale.
Tabella Riassuntiva
• Adapter: fa lavorare insieme sistemi incompatibili. • Bridge: disaccoppia l'astrazione dall'implementazione. • Composite: costruisce strutture ad albero. • Decorator: aggiunge comportamento senza modificare le classi. • Facade: semplifica sottosistemi complessi. • Flyweight: condivide i dati per risparmiare memoria. • Proxy: controlla l'accesso agli oggetti.
La Regola d'Oro: Usa questi pattern per rendere il codice manutenibile. Non usarli solo per mettersi in mostra.
In seguito, inizieremo la serie sui Pattern di progettazione comportamentale.
Mahdi Shamlou: Structural Design Patterns 2026 - Facade, Flyweight, Proxy (Esempi in produzione)
I pattern di progettazione strutturali si concentrano su come le classi e gli oggetti vengono composti per formare strutture più grandi e complesse. Questi pattern aiutano a garantire che, quando le parti di un sistema cambiano, le altre parti non debbano necessariamente cambiare anche loro.
In questo articolo, esploreremo tre pattern fondamentali: Facade, Flyweight e Proxy, analizzando i problemi che risolvono e fornendo esempi pratici pronti per la produzione.
1. Facade Pattern
Cos'è il Pattern Facade?
Il pattern Facade fornisce un'interfaccia semplificata a un insieme di interfacce in un sottosistema complesso. Invece di costringere un client a interagire con decine di classi diverse e gestire la loro interdipendenza, il client interagisce con una singola classe "facciata" che delega le chiamate ai componenti appropriati.
Il Problema
Immagina di dover gestire un sistema di pagamento complesso. Per completare un acquisto, devi:
- Verificare l'inventario.
- Elaborare il pagamento.
- Gestire la spedizione.
- Inviare una notifica di conferma.
Senza una Facade, il client dovrebbe conoscere e gestire ogni singolo modulo, rendendo il codice fragile e difficile da mantenere.
La Soluzione
Creiamo una classe PaymentFacade che espone un unico metodo processOrder(). Il client chiamerà solo questo metodo, senza preoccuparsi della complessità sottostante.
Esempio in Produzione (TypeScript)
// Sottosistema: Inventario
class InventoryService {
checkStock(productId: string): boolean {
console.log(`Verifica disponibilità per il prodotto: ${productId}`);
return true;
}
}
// Sottosistema: Pagamenti
class PaymentService {
processPayment(amount: number): boolean {
console.log(`Elaborazione del pagamento di ${amount}€...`);
return true;
}
}
// Sottosistema: Spedizioni
class ShippingService {
scheduleShipment(productId: string): void {
console.log(`Spedizione programmata per il prodotto: ${productId}`);
}
}
// La Facade
class OrderFacade {
private inventory = new InventoryService();
private payment = new PaymentService();
private shipping = new ShippingService();
public processOrder(productId: string, amount: number): void {
console.log("--- Inizio processo ordine ---");
if (this.inventory.checkStock(productId)) {
if (this.payment.processPayment(amount)) {
this.shipping.scheduleShipment(productId);
console.log("Ordine completato con successo!");
} else {
console.log("Errore durante il pagamento.");
}
} else {
console.log("Prodotto non disponibile.");
}
console.log("--- Fine processo ordine ---\n");
}
}
// Utilizzo del Client
const orderFacade = new OrderFacade();
orderFacade.processOrder("laptop-123", 1200);
2. Flyweight Pattern
Cos'è il Pattern Flyweight?
Il pattern Flyweight viene utilizzato per ridurre drasticamente l'utilizzo della memoria condividendo parti comuni di stato tra molti oggetti simili. È particolarmente utile quando si devono gestire migliaia o milioni di oggetti che condividono dati immutabili.
Il Problema
Immagina un videogioco con una foresta composta da 100.000 alberi. Se ogni oggetto Tree memorizza non solo la sua posizione (stato variabile), ma anche la sua texture, il modello 3D e i dati della foglia (stato intrinseco), la memoria RAM si esaurirebbe rapidamente.
La Soluzione
Dividiamo lo stato dell'oggetto in due parti:
- Stato Intrinseco: Dati condivisi e immutabili (es. texture, modello).
- Stato Estrinseco: Dati unici per ogni istanza (es. coordinate X, Y).
Utilizziamo una "Flyweight Factory" per gestire e condividere gli oggetti che contengono lo stato intrinseco.
Esempio in Produzione (TypeScript)
// Stato Intrinseco: Dati condivisi tra molti alberi
class TreeType {
constructor(
public readonly name: string,
public readonly color: string,
public readonly texture: string
) {}
}
// Flyweight Factory
class TreeFactory {
private static treeTypes: { [key: string]: TreeType } = {};
public static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}-${color}-${texture}`;
if (!this.treeTypes[key]) {
console.log(`Creazione nuovo tipo di albero: ${name}`);
this.treeTypes[key] = new TreeType(name, color, texture);
}
return this.treeTypes[key];
}
}
// Oggetto Flyweight (con stato estrinseco)
class Tree {
constructor(
private x: number,
private y: number,
private type: TreeType
) {}
public draw(): void {
console.log(`Disegno un ${this.type.name} (${this.type.color}) in posizione [${this.x}, ${this.y}]`);
}
}
// Utilizzo del Client
const forest: Tree[] = [];
// Creiamo molti alberi, ma useremo solo pochi tipi di Flyweight
for (let i = 0; i < 5; i++) {
const type = TreeFactory.getTreeType("Quercia", "Verde", "Texture_Foglia_HD");
forest.push(new Tree(Math.random() * 100, Math.random() * 100, type));
}
for (let i = 0; i < 5; i++) {
const type = TreeFactory.getTreeType("Pino", "Verde Scuro", "Texture_Pino_HD");
forest.push(new Tree(Math.random() * 100, Math.random() * 100, type));
}
forest.forEach(tree => tree.draw());
3. Proxy Pattern
Cos'è il Pattern Proxy?
Il pattern Proxy fornisce un sostituto o un segnaposto per un altro oggetto per controllare l'accesso ad esso. Un proxy può agire come un intermediario per gestire compiti come il caricamento pigro (lazy loading), il controllo dei permessi o il caching.
Il Probleimento
Immagina un'applicazione che deve caricare immagini ad alta risoluzione da un server remoto. Se carichi tutte le immagini all'avvio, l'applicazione diventerà lenta e consumerà troppa banda, anche se l'utente non visualizzerà mai tutte le immagini.
La Soluzione
Utilizziamo un Proxy che agisce come un "impostore" dell'oggetto reale. Il proxy carica l'oggetto reale solo quando è strettamente necessario (Lazy Loading).
Esempio in Produzione (TypeScript)
// Interfaccia comune
interface Image {
display(): void;
}
// Oggetto Reale (pesante da caricare)
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
this.loadFromDisk();
}
private loadFromDisk(): void {
console.log(`Caricamento immagine pesante: ${this.filename}...`);
}
public display(): void {
console.log(`Visualizzazione immagine: ${this.filename}`);
}
}
// Proxy
class ProxyImage implements Image {
private realImage: RealImage | null = null;
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
public display(): void {
// Lazy Loading: l'oggetto reale viene creato solo alla prima chiamata di display()
if (this.realImage === null) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// Utilizzo del Client
console.log("Inizializzazione applicazione...");
const image1 = new ProxyImage("foto_vacanze_hd.jpg");
const image2 = new ProxyImage("panorama_montagna_4k.png");
// L'immagine non è ancora stata caricata in memoria
console.log("Applicazione pronta. Nessuna immagine caricata ancora.");
// L'immagine viene caricata solo ora
image1.display();
// La seconda immagine viene caricata solo quando richiesta
image2.display();
// La prima immagine è già in memoria, quindi viene visualizzata istantaneamente
image1.display();
Conclusione
I pattern strutturali sono strumenti essenziali per scrivere codice scalabile e manutenibile:
| Pattern | Scopo Principale | Caso d'uso tipico |
|---|---|---|
| Facade | Semplificare l'interfaccia di un sistema complesso. | API Gateway, Wrapper di librerie esterne. |
| Flyweight | Ottimizzare l'uso della memoria condividendo dati. | Motori grafici, sistemi di gestione documenti massivi. |
| Proxy | Controllare l'accesso a un oggetto. | Lazy loading, Caching, Sicurezza/Autorizzazione. |
Scegliere il pattern corretto dipende dal problema specifico che stai cercando di risolvere: la complessità dell'interfaccia, il consumo di memoria o il controllo dell'accesso.