Introduzione ai design pattern

Da Riccardo Golia – Microsoft MVP Solutions Architect

Progettare applicazioni basate sul paradigma object-oriented non è affatto banale, riuscire a renderle anche riusabili ed estendibili a piacimento è ancora più arduo. Ciò che occorre di volta in volta è saper individuare gli oggetti giusti, con un livello di dettaglio e granularità adeguato, ed essere in grado di definire una struttura gerarchica consistente, basata su un insieme di relazioni e collaborazioni coerente ed efficace.

Riuscire nell'intento di definire al primo tentativo una struttura ad oggetti che sia corretta e al tempo stesso riusabile e flessibile è un'impresa quasi impossibile, soprattutto nel caso di applicazioni particolarmente complesse. In genere, durante la sua realizzazione, un'applicazione subisce innumerevoli modifiche dettate dalla variabilità delle specifiche progettuali, dalla scarsa conoscenza del dominio applicativo e dall'inesperienza. In più la mancanza di tempo, che nei progetti di sviluppo è un aspetto quasi congenito, porta sovente a scegliere soluzioni molto focalizzate sul modello reale attuale e poco orientate ad adattarsi ai cambiamenti futuri.

In uno scenario come quello descritto, diventa importante sia per chi progetta, sia per chi scrive il codice saper individuare delle soluzioni che siano riutilizzabili più volte nell'ambito di uno stesso progetto, piuttosto che in progetti diversi, senza ogni volta dover partire da zero per risolvere uno specifico problema.

Per minimizzare il lavoro da svolgere, gli sviluppatori meno esperti solitamente tendono a ricorrere a tecniche non object-oriented (non ultimo, il famigerato copia-e-incolla), con il risultato di duplicare parti di codice e di introdurre in modo più o meno voluto accoppiamento e dipendenze tra gli oggetti. Gli architetti e gli sviluppatori con più esperienza tendono invece a preferire le soluzioni object-oriented che in passato si sono rivelate vincenti ed efficaci.

Queste soluzioni, che in prima analisi possiamo definire pattern, sono orientate a risolvere particolari problematiche di progettazione e tendono ad introdurre nell'ambito di una struttura ad oggetti quella flessibilità che è necessaria per rendere il codice riutilizzabile ed estendibile.

 

Pattern architetturali, design pattern e idiomi

Non tutti i pattern sono uguali ed è bene capire come essi possono essere strutturati e organizzati. Dal momento che esistono diverse tipologie di pattern in funzione della loro area di applicazione, in generale essi possono essere raggruppati in macrocategorie specifiche (dette anche cluster), ciascuna delle quali contenente pattern orientati a risolvere problematiche similari. I cluster possono a loro volta essere suddivisi in sottocategorie a granularità più bassa.

Oltre che l’appartenenza ad un determinato cluster, per un pattern è possibile considerare come fattore distintivo anche il livello di astrazione che lo contraddistingue. Nell’ambito del cluster dei pattern relativi allo sviluppo di applicazioni software possiamo individuare tre categorie di pattern caratterizzate da un diverso livello di astrazione.

  • Pattern architetturali: descrivono lo schema organizzativo della struttura che caratterizza un sistema software. In genere questi pattern individuano le parti del sistema a cui sono associate responsabilità omogenee e le relazioni che esistono tra i diversi sottosistemi. Un esempio significativo di pattern architetturale è rappresentato dal layering, che descrive come suddividere un’applicazione in strati logici sovrapposti e tra loro comunicanti.

  • Pattern di disegno (design pattern): sono i pattern che si riferiscono alle problematiche legate al disegno object-oriented e di essi avremo modo di parlare in modo più approfondito nel corso dell’articolo.

  • Pattern di implementazione (idiomi): sono pattern di basso livello specifici per una particolare tecnologia (per esempio, il .NET Framework). Essi descrivono le modalità implementative da utilizzare per risolvere problematiche di sviluppo sfruttando in modo mirato le caratteristiche peculiari di una particolare piattaforma.

Come detto, ciascuno di questi gruppi è caratterizzato da un grado di astrazione differente. I design pattern si collocano tra i pattern architetturali, troppo generici per essere orientati a risolvere problematiche di disegno, e i pattern idiomatici, molto legati alla tecnologia a cui si riferiscono e all’implementazione vera e propria. I design pattern descrivono soluzioni che lasciano sempre e comunque un certo grado di libertà nella loro adozione e implementazione, dal momento che non descrivono mai soluzioni che sono valide per una piattaforma specifica, ma al contrario hanno una validità più generale e trasversale rispetto alla tecnologia.

 

Il significato dei design pattern

Uno degli aspetti più delicati nel disegno object-oriented (OOD) consiste nella scomposizione del sistema in oggetti. Si tratta di una attività complessa dal momento che entrano in gioco fattori non direttamente collegati alle specifiche funzionali quali l'accoppiamento tra oggetti, la loro dipendenza, la coesione funzionale, la granularità, la flessibilità, l'estendibilità e la riusabilità. Questi aspetti devono necessariamente influenzare il processo di scomposizione, talvolta anche in modi tra loro discordanti.

Esistono diversi approcci che permettono di scomporre in oggetti un sistema. È possibile partire dai casi d'uso, individuare in essi i sostantivi e i verbi e da questi ricavare le classi e i metodi corrispondenti al fine di ottenere la struttura ad oggetti desiderata. In alternativa, è possibile porre l'attenzione principalmente sulle responsabilità e sulle collaborazioni nell'ambito del sistema in fase di studio e, in funzione di esse, individuare gli oggetti necessari per gestirle opportunamente. O ancora, è possibile partire da un modello del mondo reale e tradurre gli elementi individuati in altrettanti oggetti.

Ognuno di questi approcci concorre a definire la struttura statica del sistema che, se da un lato rispecchia la realtà di oggi, dall'altro in genere non si presta direttamente ad evolvere nel tempo e ad adattarsi alla realtà di domani. L'esigenza di individuare una struttura flessibile ed estendibile, in grado di rispondere al meglio ai cambiamenti nel tempo, porta inevitabilmente a cercare soluzioni di disegno che facciano largo uso di astrazioni e oggetti non necessariamente collegati alla realtà in esame per non limitare in modo importante l'evoluzione del sistema.

I design pattern aiutano ad individuare queste astrazioni e gli oggetti in grado di rappresentarle. Essi concorrono a limitare le dipendenze, incrementando l'estendibilità e la riusabilità, e rendono di conseguenza il sistema più manutenibile nel tempo, in grado di rispondere ai cambiamenti in modo efficiente ed elegante.

I design pattern agevolano il riuso di soluzioni architetturali note, rendendo accessibili agli architetti e agli sviluppatori tecniche di disegno universalmente riconosciute come valide ed efficaci. In questo senso i design pattern aiutano i progettisti a operare scelte consapevoli tra le varie alternative possibili allo scopo di favorire la riusabilità.

 

La storia dei design pattern

Il concetto di pattern scaturisce dal lavoro di un architetto edile, Christopher Alexander, che alla fine degli anni '70 fu il primo a proporre l'idea di usare i pattern, ovvero soluzioni collaudate a problematiche note, per progettare l'architettura di palazzi ed edifici in genere. Successivamente, nel corso degli anni '80 e nei successivi anni '90, l'argomento fu ripreso e sviluppato dagli esperti informatici del tempo nell'ottica della progettazione object-oriented, fino alla definitiva consacrazione nel 1995 coincidente con l'uscita del libro Design Patterns: Elements of Reusable Object-Oriented Software di Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (conosciuti anche come la Gang of Four, GoF), che rappresenta ancor'oggi una delle pietre miliari della letteratura dedicata alla progettazione di sistemi basati sul paradigma ad oggetti. Negli ultimi anni molti autori hanno contribuito con ulteriori pubblicazioni dedicate al tema dei design pattern, fornendo nuove interpretazioni e ulteriori spunti di riflessione in merito al loro utilizzo.

 

Elementi caratterizzanti dei design pattern

Riprendendo quanto detto da Alexander nel suo libro A Pattern Language, "ogni pattern descrive un problema che si ripete più e più volte nel nostro ambiente, descrive quindi il nocciolo della soluzione del problema, in modo tale che la soluzione possa essere usata un milione di volte, senza che essa venga mai applicata nella stessa maniera". Come si può comprendere dalla definizione di Alexander, il concetto che sta alla base dei pattern è quello di fornire una soluzione ad un problema in un determinato contesto. Nel caso della progettazione del software, questo significa individuare meccanismi e tecniche che permettano di risolvere problematiche di disegno object-oriented in modo elegante, ripetibile ed efficace.

In genere un design pattern è caratterizzato da quattro elementi fondamentali.

  • Nome: descrive le funzionalità di un pattern con una o due parole. Associare un nome ad un pattern permette di identificarlo in modo semplice ed immediato e consente di condividere le idee di disegno ad un livello più alto di astrazione, senza la necessità di dover entrare nei dettagli implementativi.

  • Problema: descrive la situazione alla quale applicare il pattern e le condizioni necessarie e propedeutiche all'utilizzo del pattern stesso.

  • Soluzione: descrive in modo astratto come il pattern risolve il problema, specificando gli elementi coinvolti con le loro responsabilità e collaborazioni. La soluzione viene solitamente espressa in modo sufficientemente generale da lasciare numerosi gradi di libertà nelle possibili scelte implementative. Un pattern infatti è come uno schema che può essere applicato ripetutamente, il più delle volte in modo particolare e differente.

  • Conseguenze: descrive l'insieme dei risultati e dei vincoli a cui si va incontro nell'applicazione del pattern. Le conseguenze sono fondamentali per poter valutare i vantaggi e gli svantaggi derivanti dall'uso del pattern e per poter eventualmente preferire soluzioni alternative per la risoluzione del problema.

Un design pattern associa un nome identificativo ad un problema di progettazione, permette di identificare gli elementi che concorrono a definire la struttura ad oggetti a cui il pattern si riferisce e, per ciascun elemento individuato, specifica il ruolo, le collaborazioni e le dipendenze con altri oggetti e, in generale, le responsabilità ad esso attribuite. Ciascun design pattern è focalizzato su una particolare problematica di disegno e per essa specifica i possibili scenari di utilizzo, evidenziandone i vincoli e le conseguenze.

 

Il cluster dei pattern GoF

Tra i vari design pattern noti in letteratura, i pattern GoF (Gang of Four) formano senza dubbio un cluster fondamentale. Conoscere i nomi e le motivazioni di questi pattern rappresenta senza dubbio un buon punto di partenza per poter successivamente approfondire i dettagli che li riguardano ed eventualmente valutarne l’utilizzo.

I 23 pattern che compongono questo cluster sono organizzati in tre categorie distinte e tra loro complementari:

  • pattern creazionali, che riguardano la creazione di istanze;

  • pattern strutturali, che si riferiscono alla composizione di classi e oggetti;

  • pattern comportamentali, che si occupano delle modalità con cui classi e oggetti interagiscono tra loro in relazione alle loro diverse responsabilità.

I pattern creazionali sono cinque:

  • Abstract Factory: fornisce un’interfaccia per creare famiglie di oggetti correlati o dipendenti senza specificare le classi concrete;

  • Builder: separa la costruzione di un oggetto complesso dalla sua rappresentazione, in modo tale che lo stesso processo di costruzione possa creare rappresentazioni differenti;

  • Factory Method: definisce un’interfaccia per creare un oggetto, ma lascia alle classi derivate di decidere quale classe istanziare. Questo pattern permette a una classe di delegare la creazione di un’istanza alle sue classi derivate;

  • Prototype: specifica il tipo degli oggetti da creare usando un’istanza prototipale e crea i nuovi oggetti a partire da questo prototipo;

  • Singleton: assicura che una classe abbia solamente un’unica istanza e fornisce un entry-point globale ad essa.

I pattern strutturali sono sette:

  • Adapter: converte l’interfaccia di una classe in un’altra interfaccia compatibile con il client. Questo pattern consente a classi diverse di collaborare tra loro, cosa che non sarebbe possibile diversamente a causa della incompatibilità delle rispettive interfacce;

  • Bridge: disaccoppia un’astrazione dalla sua implementazione affinché entrambe possano variare in modo indipendente;

  • Composite: compone una serie di oggetti in una struttura ad albero secondo una gerarchia di tipo part-whole (parte-totalità). Questo pattern permette ai client di trattare oggetti singoli o loro raggruppamenti in modo uniforme;

  • Decorator: aggiunge dinamicamente responsabilità addizionali ad un oggetto. Questo pattern fornisce un meccanismo alternativo e flessibile all’ereditarietà per estendere le funzionalità base;

  • Facade: fornisce un’interfaccia unificata a un insieme di interfacce in un sottosistema. Questo pattern definisce un’interfaccia ad un livello più alto che rende il sottosistema più facile da usare, dato che ne maschera la complessità interna;

  • Flyweight: usa la condivisione per gestire in modo efficiente un numero considerevole di oggetti a granularità fine;

  • Proxy: fornisce un surrogato di un oggetto per controllare l’accesso ad esso.

I pattern comportamentali sono undici:

  • Chain of Responsability: evita di accoppiare il mittente di una richiesta con il suo destinatario dando la possibilità a più di un oggetto di gestire la richiesta. Collega tra loro gli oggetti ricevitori e fa passare la richiesta da un oggetto all’altro fino a destinazione;

  • Command: incapsula una richiesta in un oggetto, rendendo possibile parametrizzare i client con diverse tipologie di richieste, con richieste bufferizzate (queue), con richieste registrate (log) e con richieste annullabili (undo);

  • Interpreter: dato un linguaggio, definisce una rappresentazione della sua grammatica e del relativo interprete, che usa la rappresentazione per interpretare le frasi del linguaggio;

  • Iterator: fornisce un modo per accedere in modo sequenziale agli elementi di una collezione di oggetti senza esporre la sua rappresentazione sottostante;

  • Mediator: definisce un oggetto che incapsula le modalità di interazione di un insieme di oggetti. Questo pattern favorisce un basso accoppiamento, evitando che gli oggetti facciano riferimento l’uno con l’altro esplicitamente, e permette di variare le modalità di interazione in modo indipendente dagli oggetti stessi;

  • Memento: senza violare l’incapsulamento, recupera e rende esplicito lo stato interno di un oggetto in modo tale che l’oggetto stesso possa essere riportato allo stato originale in un secondo momento;

  • Observer: definisce una dipendenza uno-a-molti fra oggetti in modo tale che, se un oggetto cambia stato, tutti gli oggetti da esso dipendenti vengono notificati e aggiornati automaticamente;

  • State: permette ad un oggetto di modificare il suo comportamento quando il suo stato interno cambia;

  • Strategy: definisce una famiglia di algoritmi, li incapsula e li rende intercambiabili fra loro. Questo pattern permette di variare gli algoritmi in modo indipendente dal contesto di utilizzo;

  • Template Method: definisce lo scheletro di un algoritmo in un metodo di una classe base, delegando alcuni passi alle classi derivate. Questo pattern permette di ridefinire nelle classi derivate alcuni passi dell’algoritmo senza cambiare la struttura dell’algoritmo stesso;

  • Visitor: rappresenta un’operazione da svolgersi sugli elementi di una struttura ad oggetti. Questo pattern consente di definire nuove operazioni senza cambiare le classi degli elementi su cui opera.

 

Conclusioni

In questo articolo abbiamo visto come i design pattern rappresentino un modo elegante e flessibile per fare disegno object-oriented. Essi possono essere usati sia in fase di progettazione di un sistema, sia per rivedere e migliorare un sistema già esistente applicando il refactoring al codice. Sia in un caso che nell’altro, i design pattern vanno comunque applicati con attenzione e cognizione di causa.

I design pattern non rappresentano la panacea di tutti i mali nella progettazione del software, né tanto meno vanno usati a priori in modo estensivo con l’inevitabile conseguenza di produrre over-engineering (sovradimensionamento). L’approccio corretto deve sempre porre al centro la ricerca da parte del progettista delle soluzioni di disegno più semplici, sfruttando i pattern in modo mirato per ottenere questo obiettivo.

Pertanto i design pattern vanno usati solamente quando effettivamente servono. In caso contrario il rischio è quello di produrre inutilmente una struttura ad oggetti troppo complessa e articolata, di difficile comprensione e tale da richiedere grossi sforzi per essere gestita nel tempo. L’esperienza e il buon senso in molti casi permettono di fare la scelta giusta.

 

Riferimenti utili

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides – Design Patterns: Elements of Reusable Object-Oriented Software – Addison Wesley, 1995.

  • Martin Fowler – Analysis Patterns: Reusable Object Models – Addison-Wesley, 1997.

  • Martin Fowler – Patterns of Enterprise Application Architecture – Addison-Wesley, 2003.

  • Microsoft Pattern & Practices – Enterprise Solution Patterns Using Microsoft .NET – Microsoft Corporation, 2003.

  • Craig Larman – Applying UML and Patterns – Prentice-Hall, 2001.

  • Joshua Kerievsky – Refactoring to Patterns – Addison-Wesley, 2005.

  • Sezione dedicata ai design pattern su UGIdotNETWiki