IT EN

Design pattern: la scheda tecnica del pattern più usato dagli ingegneri (anche fuori dal software)

La Gang of Four, Christopher Alexander, il ponte H e il partitore di tensione: che cos'è davvero un design pattern, spiegato con la scheda tecnica della GoF applicata a sé stessa.

Copertina in stile disegno tecnico su carta cianografica: diagramma UML del pattern Strategy con quote e cartiglio «Gang of Four: il manuale del già visto»

Nel 1994 quattro informatici pubblicano un libro con un titolo da manuale universitario, Design Patterns: Elements of Reusable Object-Oriented Software, e un'idea semplice: dare un nome alle soluzioni che i bravi programmatori reinventano da anni. Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides diventano subito la Gang of Four (GoF), un soprannome preso in prestito dalla politica cinese e decisamente più minaccioso di quanto meritassero. Il libro vende centinaia di migliaia di copie, viene tradotto in più di dieci lingue e a ottobre compirà 31 anni.

Ogni pattern del libro è descritto con lo stesso schema fisso: nome, intento, motivazione, applicabilità, struttura, conseguenze, usi noti... Per spiegare che cos'è un design pattern, quindi, useremo proprio quello schema. Questo articolo è la scheda del pattern "Design Pattern". Sì, è ricorsivo. Ce ne occuperemo nella sezione Conseguenze.

Nome e classificazione

Design Pattern. Categoria: meta-comportamentale, cioè un pattern che cambia il comportamento di chi progetta, non del codice. Si applica a tutta l'ingegneria, non solo al software.

Intento

Dare un nome a una soluzione ricorrente per un problema ricorrente in un contesto preciso, così che chi progetta possa riusare l'idea anche quando non può riusare il codice.

Questa è la parte che di solito si dimentica. Un pattern non è una libreria, non è un pezzo di codice da copiare e non è un obbligo. È un pezzo di vocabolario. Dire «qui ci metterei un Observer» sostituisce dieci minuti di spiegazione alla lavagna, esattamente come un elettronico dice «partitore di tensione» invece di «due resistenze in serie con l'uscita presa nel punto di mezzo».

Anche noto come

"Buona pratica", "il solito trucco", "l'abbiamo sempre fatto così", "quella cosa che ha fatto Marco nel 2019 e nessuno osa toccare". Nella sua forma degenere: "ci serve una Factory", pronunciato prima di sapere cosa bisogna costruire.

Motivazione

L'idea non nasce nell'informatica ma nell'architettura, quella con i mattoni. Nel 1977 Christopher Alexander pubblica A Pattern Language: 253 pattern per progettare città, edifici e stanze. Ognuno descrive un problema, le forze in gioco e una soluzione. Il pattern 159, per esempio, si chiama Light on Two Sides of Every Room: le stanze con finestre su un solo lato sono sgradevoli, quindi mettetene su due lati. Non dice dove, né di che misura, né di che materiale. Descrive la forma della soluzione, e la realizzazione concreta dipende dal contesto.

Ogni disciplina ingegneristica matura ha un catalogo del genere, anche se non lo chiama così:

  • Elettronica: resistenza di pull-up, partitore di tensione, ponte H per invertire un motore, specchio di corrente, circuito di debounce per i pulsanti;
  • Meccanica: quadrilatero articolato, cricchetto, riduttore, giunto cardanico;
  • Ingegneria civile: travature reticolari Warren e Pratt, arco, mensola, giunto di dilatazione;
  • Idraulica: valvola di non ritorno, vaso di espansione, by-pass;
  • Sistemi di sicurezza: ridondanza 2-su-3, fail-safe, watchdog.

In tutti questi casi il nome indica la stessa cosa: una relazione tra componenti che risolve un problema noto, con vantaggi e svantaggi noti. Nessuno compra "un ponte H" già pronto in un formato universale: lo si realizza ogni volta con i transistor, le tensioni e le correnti di quel progetto. È la stessa situazione del software, dove un Observer in Java, in JavaScript e in C ha un aspetto completamente diverso ma la stessa forma.

Nel 1987 Kent Beck e Ward Cunningham presentano a OOPSLA un breve lavoro in cui applicano le idee di Alexander alla programmazione a oggetti. Sette anni dopo arriva il libro della GoF, con 23 pattern raccolti da sistemi reali scritti in C++ e Smalltalk. Ward Cunningham, per raccogliere i pattern della comunità, nel 1995 crea un sito che chiunque può modificare e lo chiama WikiWikiWeb. Il primo wiki della storia nasce quindi per catalogare design pattern.

Applicabilità

Usate un pattern quando:

  • riconoscete il problema, non solo la soluzione. «Ho bisogno di cambiare algoritmo a runtime» porta a Strategy. «Voglio usare Strategy» non porta da nessuna parte;
  • la soluzione deve essere comunicata a qualcun altro, come un collega, un revisore o voi stessi tra sei mesi;
  • il linguaggio che usate non risolve già il problema da solo (ci torniamo in Conseguenze).

Non usatelo per far sembrare il codice "enterprise", né per riempire il CV.

Struttura

I 23 pattern della GoF sono divisi in tre famiglie. Per ognuno, ecco l'equivalente in un'altra ingegneria, che spesso è la spiegazione migliore:

FamigliaPatternEquivalente fisico
Creazionali (5)Abstract Factory, Factory Method, BuilderUna linea di montaggio: chiedi "un'auto", la configurazione la decide la fabbrica
Prototype, SingletonLo stampo da cui si copiano i pezzi; il contatore generale di casa (ce n'è uno, e guai a chi ne aggiunge un secondo)
Strutturali (7)AdapterL'adattatore per le prese che portate in vacanza nel Regno Unito
FacadeIl cruscotto dell'auto: un pedale al posto di iniezione, accensione e farfalla
Decorator, ProxyIl filtro davanti all'obiettivo; il citofono davanti alla porta
Composite, Bridge, FlyweightLa distinta base (un assieme è fatto di sottoassiemi e pezzi); il telecomando universale; i caratteri mobili della tipografia
Comportamentali (11)ObserverIl campanello di una scuola: chi deve sentirlo lo sente
Strategy, State, Template MethodLe punte intercambiabili del trapano; il cambio dell'auto; la ricetta con "aggiungere ingrediente a scelta"
Command, Memento, Chain of Responsibility, Iterator, Mediator, Visitor, InterpreterLo scontrino della comanda al ristorante; il punto di ripristino; il centralino che ti passa all'ufficio giusto; il nastro trasportatore; la torre di controllo; l'ispettore che visita ogni reparto; lo spartito

La parte più importante del libro, però, non sono i 23 pattern. Sono due frasi del primo capitolo, che valgono più di tutti i diagrammi UML che seguono:

Program to an interface, not an implementation.
Favor object composition over class inheritance.

Nel linguaggio dell'ingegneria: progettate le interfacce (connettori, flange, protocolli) prima dei componenti, e costruite sistemi assemblando moduli invece di derivare una variante dall'altra. Un ingegnere meccanico lo chiamerebbe semplicemente buon senso.

Partecipanti

  • Il problema, che si ripresenta abbastanza spesso da meritare un nome;
  • Il contesto, cioè i vincoli che rendono la soluzione sensata qui e non altrove;
  • Le forze, ovvero gli obiettivi in conflitto: flessibilità contro semplicità, prestazioni contro estendibilità;
  • La soluzione, che è una forma e non un'implementazione;
  • Il nome, il partecipante più sottovalutato e più prezioso.

Codice d'esempio

Il pattern Strategy nella sua forma originale, come lo si scriveva nel 1994 (qui in Java, che arriverà l'anno dopo):

interface Compressor { byte[] compress(byte[] data); }

class GzipCompressor implements Compressor {
    public byte[] compress(byte[] data) { /* ... */ return data; }
}
class ZstdCompressor implements Compressor {
    public byte[] compress(byte[] data) { /* ... */ return data; }
}

class Archiver {
    private final Compressor compressor;          // composizione, non ereditarietà
    Archiver(Compressor c) { this.compressor = c; }
    byte[] pack(byte[] data) { return compressor.compress(data); }
}

new Archiver(new ZstdCompressor()).pack(bytes);

Lo stesso pattern in un linguaggio con le funzioni come valori:

def pack(data, compress=zstd.compress):
    return compress(data)

pack(data, compress=gzip.compress)

Quattro classi diventano un parametro. Il pattern c'è ancora (un algoritmo intercambiabile dietro un'interfaccia), ma è diventato invisibile. E il kernel Linux, scritto in C, usa Strategy da sempre senza chiamarlo così:

/* ogni file system fornisce la propria "strategia" di lettura e scrittura */
static const struct file_operations ext4_file_operations = {
    .read_iter  = ext4_file_read_iter,
    .write_iter = ext4_file_write_iter,
    .mmap       = ext4_file_mmap,
    /* ... */
};

Una struct di puntatori a funzione è una tabella virtuale scritta a mano. È lo stesso pattern: le tabelle di puntatori a funzione per i driver esistevano già nello Unix degli anni '70, molto prima che qualcuno disegnasse un diagramma UML.

Conseguenze

Vantaggi. Un vocabolario comune, decisioni di progetto più rapide e codice che chi conosce i pattern riconosce subito. Quando vedete new BufferedReader(new InputStreamReader(System.in)) capite che è un Decorator, e sapete già come estenderlo.

Svantaggi. Il primo è l'abuso. Un programmatore che ha appena letto il libro tende a vedere pattern ovunque. Il risultato può essere una classe come AbstractSingletonProxyFactoryBean, che esiste davvero in Spring, o progetti parodia come FizzBuzz Enterprise Edition, che risolve un esercizio da dieci righe con decine di factory, strategy e visitor. Il Singleton in particolare è una variabile globale in giacca e cravatta, e la maggior parte dei test che non si riescono a scrivere ha un Singleton da qualche parte.

Il secondo svantaggio è più sottile. Nel 1996 Peter Norvig mostra che 16 dei 23 pattern diventano invisibili o molto più semplici in un linguaggio dinamico come Lisp o Dylan. Paul Graham va oltre: se nel suo codice vede dei pattern, lo considera un segnale che sta facendo a mano il lavoro di un compilatore. Gli ultimi trent'anni di storia dei linguaggi gli danno in parte ragione:

  • Iterator è diventato for (x : xs), for x in xs e for...of;
  • Strategy e Command sono diventati lambda e closure (Java ci arriva solo nel 2014, con la versione 8);
  • Observer è diventato addEventListener, i signal, gli stream reattivi;
  • Visitor si sta trasformando in pattern matching: match in Rust e Python, switch con sealed interface in Java 21.

Un pattern, quindi, è spesso una funzionalità del linguaggio che non esiste ancora. Ma questo non lo rende inutile: il nome resta anche quando il codice sparisce. Nessuno scrive più una classe Iterator, eppure tutti sanno cos'è un iteratore.

Il terzo svantaggio è la ricorsione promessa all'inizio. Un pattern che descrive i pattern dovrebbe contenere sé stesso. Lo fa: la regola «documenta le soluzioni ricorrenti con un nome, un problema, un contesto e delle conseguenze» è a sua volta una soluzione ricorrente con un nome. Abbiamo verificato che il caso base esiste, quindi lo stack non va in overflow.

Implementazione

Alcuni consigli pratici, raccolti da chi li ha imparati sbagliando:

  1. Partite dal problema. Scrivete prima il codice più semplice che funzioni. Se dopo la terza modifica simile vi accorgete che state riscrivendo sempre lo stesso if, quello è il momento di introdurre un pattern. È la "regola del tre" del refactoring.
  2. Usate il nome nel codice. PaymentStrategy e OrderObserver dicono subito a chi legge come funziona quella parte del progetto. Non serve aggiungere il nome del pattern a ogni classe, però: UserFactoryFactory è un indizio che qualcosa è andato storto.
  3. Conoscete anche gli anti-pattern. Il termine lo usa Andrew Koenig nel 1995, e il catalogo è altrettanto utile: God Object, Big Ball of Mud (Foote e Yoder, 1997), Golden Hammer, cioè «quando hai un martello d'oro, tutto sembra un chiodo».
  4. Il pattern non è il diagramma. Le figure UML del libro mostrano una realizzazione possibile, non quella obbligatoria. Come per il ponte H, conta la relazione tra le parti.

Usi noti

  • Smalltalk-80 MVC: il primo capitolo del libro lo analizza come combinazione di Observer (il modello avvisa le viste), Composite (viste annidate) e Strategy (il controller è intercambiabile);
  • Il DOM del browser: un Composite (ogni nodo contiene altri nodi) con Observer (gli eventi) e Chain of Responsibility (la propagazione degli eventi verso l'alto);
  • Le pipe di Unix: Pipes and Filters, un pattern architetturale descritto due anni dopo in Pattern-Oriented Software Architecture;
  • I sistemi distribuiti: Circuit Breaker, reso popolare da Michael Nygard in Release It! (2007), si chiama così proprio per l'interruttore magnetotermico. Dopo troppi errori "salta" e smette di chiamare il servizio guasto, poi dopo un po' riprova. È il percorso inverso: un pattern che il software ha preso dall'impianto elettrico;
  • Gli autori stessi: Erich Gamma scrive JUnit con Kent Beck e poi guida lo sviluppo di Eclipse JDT e di VS Code. Ralph Johnson diventa uno dei padri del refactoring. John Vlissides, scomparso nel 2005, ha lasciato una rubrica sui pattern che ancora oggi è una lettura piacevole.

Pattern correlati

Standard: un pattern che una comunità ha reso obbligatorio, con tanto di numero ISO. Framework: un insieme di pattern già implementati, in cui siete voi a essere chiamati dal codice (la "Hollywood principle": don't call us, we'll call you). Ricetta di cucina: un pattern con le dosi, che funziona finché il forno è quello dell'autore.

Morale ingegneristica: i design pattern non sono una specialità del software, sono il modo in cui ogni ingegneria trasforma l'esperienza in un linguaggio comune. Quello che la Gang of Four ha portato all'informatica non sono 23 diagrammi, ma l'abitudine di dare un nome alle cose che funzionano. E quella abitudine non va mai fuori moda, anche quando il linguaggio rende inutile la classe.

Alla prossima storia di bit. Questa volta, senza Factory. 📐