Design pattern - introduzione e catalogo GoF
In questa pagina 7
Definizione (pattern). "Un pattern descrive un problema che si ripresenta di continuo nel nostro ambiente e poi ne descrive il nucleo della soluzione, in modo da poterla usare un milione di volte senza mai farlo due volte nello stesso modo" (Christopher Alexander, architetto). In sintesi: una soluzione ricorrente a un problema standard, in un contesto. Per l'ingegneria del software: una coppia nome-problema/soluzione applicabile in contesti nuovi, con consigli su come adattarla (Craig Larman).
Perché servono: nelle discipline mature i progettisti non ricavano ogni progetto dalle leggi della fisica ma riusano progetti già collaudati (i manuali, i "prontuari"); scrivere software da zero è costoso, e i pattern permettono di riusare l'esperienza di progetto, non solo il codice. Un pattern è come il cartamodello di un abito: dice che cosa si sta costruendo e come verificarlo, invece di dare le coordinate di ogni taglio. Un secondo vantaggio è il vocabolario: dire "qui c'è un Observer" comunica in una parola struttura e intenzioni. Questa nota introduce il catalogo e i principi; le famiglie sono in Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →, Pattern strutturali - Adapter, Decorator, Composite, Facade e ProxyI pattern strutturali compongono classi e oggetti in strutture più grandi: Adapter converte l'interfaccia di una classe in quella attesa dal client (di oggetto, per composizione, o di classe, per ereditarietà multipla di classe e interfaccia); Decorator aggiunge responsabilità a un oggetto con un involucro che ha la stessa interfaccia (l'ordine dei decoratori conta); Composite tratta in modo uniforme foglie e contenitori in una struttura ad albero; Facade offre un'interfaccia semplice a un sottosistema (non cerca il polimorfismo, a differenza dell'Adapter); Proxy è un sostituto con la stessa interfaccia che controlla l'accesso (virtuale, di protezione, di cache, dinamico); Bridge e Flyweight completano le sette forme del catalogo.Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy →, Pattern comportamentali - Observer, Strategy, Command, Iterator e Template MethodI pattern comportamentali descrivono come gli oggetti collaborano e si dividono le responsabilità: Observer (publish-subscribe) avvisa automaticamente i dipendenti quando il soggetto cambia stato, mantenendo basso l'accoppiamento (si registrano e cancellano a runtime, modello push o pull, attenzione alle modifiche durante la notifica); Strategy rende intercambiabili gli algoritmi dietro un'interfaccia (in Java anche con una lambda); Command trasforma la richiesta in un oggetto, quindi memorizzabile, accodabile e annullabile (undo); Iterator scorre un aggregato senza esporne la struttura (for-each, fail-fast); Template Method fissa lo scheletro di un algoritmo e lascia alle sottoclassi passi astratti o hook (principio di Hollywood); State, Visitor, Chain of Responsibility, Mediator, Memento e Interpreter completano le undici forme del catalogo.Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method →; i criteri per assegnare le responsabilità a monte dei pattern sono in Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →.
Tre famiglie di pattern, due vocabolari
Gli elementi di un pattern
Ogni pattern GoF ha quattro elementi essenziali:
| elemento | contenuto |
|---|---|
| nome | aumenta il vocabolario dei progettisti; permette di discutere senza i dettagli |
| problema | intento, contesto, quando applicarlo, sintomi di un progetto rigido |
| soluzione | classi e oggetti coinvolti, relazioni, responsabilità e collaborazioni; è un modello, non un progetto concreto né un'implementazione |
| conseguenze | risultati e compromessi (spazio, tempo, flessibilità) |
Nel libro e nelle note ogni pattern si descrive con lo stesso schema:
| sezione | contenuto |
|---|---|
| nome e classificazione | il nome del pattern e la sua famiglia |
| intento | che cosa fa, perché esiste, quale problema affronta |
| also known as | altri nomi (Adapter = Wrapper) |
| motivazione | uno scenario che illustra il problema |
| applicabilità | situazioni in cui usarlo e progetti scadenti che risolve |
| struttura | diagramma delle classi (OMT/UML) e di interazione |
| partecipanti | classi e oggetti e le loro responsabilità |
| conseguenze | vantaggi, svantaggi, quale aspetto del sistema permette di variare in modo indipendente |
| implementazione | trucchi, insidie, aspetti legati al linguaggio |
| codice d'esempio | in questa parte: Java, tutto compilato |
| usi noti | almeno due sistemi reali in domini diversi |
| pattern correlati | differenze e combinazioni |
Il catalogo: 23 pattern
Le tre famiglie e la tabella completa. I pattern in grassetto sono trattati con esempio completo nelle note successive; gli altri sono solo descritti.
Creazionali (5): come si creano gli oggetti
| pattern | intento in una riga |
|---|---|
| Singleton | garantire un'unica istanza di una classe e un punto di accesso globale |
| Factory Method | un metodo che le sottoclassi ridefiniscono per decidere quale classe istanziare |
| Abstract Factory | creare famiglie di oggetti correlati senza nominarne le classi concrete |
| Builder | costruire un oggetto complesso passo per passo, separando costruzione e rappresentazione |
| Prototype | creare nuovi oggetti clonando un'istanza prototipo |
Strutturali (7): come si compongono classi e oggetti
| pattern | intento in una riga |
|---|---|
| Adapter | convertire l'interfaccia di una classe in quella che il client si aspetta (wrapper) |
| Decorator | aggiungere responsabilità a un oggetto in modo dinamico e trasparente |
| Composite | trattare in modo uniforme oggetti singoli e composizioni ricorsive (strutture ad albero) |
| Facade | un'interfaccia semplice a un sottosistema complesso |
| Proxy | un sostituto che controlla l'accesso a un altro oggetto |
| Bridge | separare un'astrazione dalla sua implementazione perché varino indipendentemente |
| Flyweight | condividere in modo efficiente un gran numero di oggetti a grana fine |
Comportamentali (11): come collaborano gli oggetti
| pattern | intento in una riga |
|---|---|
| Observer | quando un oggetto cambia stato, i dipendenti sono avvisati e si aggiornano |
| Strategy | una famiglia di algoritmi intercambiabili |
| Command | la richiesta diventa un oggetto: si può memorizzare, accodare, annullare |
| Iterator | scorrere gli elementi di un aggregato senza esporne la struttura |
| Template Method | lo scheletro di un algoritmo con alcuni passi forniti dalle sottoclassi |
| Chain of Responsibility | la richiesta passa lungo una catena finché qualcuno la gestisce |
| Interpreter | interprete per un linguaggio con una piccola grammatica |
| Mediator | un oggetto coordina le interazioni di un gruppo di colleghi |
| Memento | catturare e ripristinare lo stato di un oggetto senza violarne l'incapsulamento |
| State | il comportamento di un oggetto dipende dal suo stato interno |
| Visitor | operazioni da applicare agli elementi di una struttura eterogenea, senza modificarne le classi |
Come si legge una tabella di struttura. Nelle note successive la struttura è data come tabella di ruoli, non con un diagramma: per ogni pattern si elencano i ruoli (per esempio Target, Adapter, Adaptee, Client), la classe del programma che li interpreta e la relazione. Per ricordare la notazione UML delle classi: ereditarietà (freccia con triangolo vuoto: is-a), realizzazione di interfaccia (tratteggiata con triangolo vuoto), associazione (linea o freccia: "usa"), aggregazione (rombo vuoto: "ha", le parti sopravvivono al tutto), composizione (rombo pieno: "è composto da", le parti muoiono con il tutto), dipendenza (freccia tratteggiata: "usa temporaneamente").
I principi dietro i pattern
I pattern sono applicazioni di pochi principi; riconoscerli aiuta a capire e a inventare varianti.
| principio | significato |
|---|---|
| programmare verso un'interfaccia (code to an interface) | le variabili e i parametri hanno il tipo di un'interfaccia o di una classe astratta, non di una classe concreta |
| incapsulare ciò che varia | si separa la parte che cambia da quella che resta uguale (la new di una classe concreta, l'algoritmo di calcolo, il canale di notifica) |
| preferire la composizione all'ereditarietà | l'ereditarietà è statica e crea sottoclassi per ogni combinazione; la composizione si cambia a runtime |
| principio aperto/chiuso (OCP) | le classi sono aperte all'estensione (aggiungere un tipo) e chiuse alla modifica (non si riscrive codice funzionante) |
| inversione delle dipendenze (DIP) | dipendere dalle astrazioni, non dalle classi concrete: sia il codice di alto livello sia quello di basso livello dipendono da un'interfaccia |
| responsabilità unica | una classe ha un solo motivo per cambiare (alta coesione, vedi Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →) |
Linee guida del corso per l'inversione delle dipendenze (sono linee guida, non regole assolute: seguite alla lettera non si riuscirebbe a produrre un sistema eseguibile):
- nessuna variabile dovrebbe contenere un riferimento a una classe concreta;
- nessuna classe dovrebbe derivare da una classe concreta;
- nessun metodo dovrebbe ridefinire un metodo già implementato di una classe base.
Il problema di new
Ogni new di una classe concreta viola "programma verso un'interfaccia": anche se la variabile ha il tipo dell'interfaccia (Duck d = new DecoyDuck()), la classe che contiene la riga dipende dalla classe concreta. Un if che sceglie quale classe creare in base a una condizione dipende da tutte le classi concrete che nomina, e va modificato (e ricompilato) a ogni nuova classe: viola l'aperto/chiuso e "incapsula ciò che varia". Le famiglie di pattern creazionali sono la risposta (Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →).
Esempio minimo (biblioteca): SollecitoRigido crea da sé una NotificaMail e per usare gli SMS bisogna modificarla; Sollecito riceve un Notificatore dall'esterno (dependency injection) e non cambia. Per aggiungere un registro (log) a qualunque canale si usa un oggetto che lo avvolge (composizione, pattern Decorator) invece di una sottoclasse per ogni coppia.
/* Principi che stanno dietro ai pattern: programmare verso un'interfaccia, incapsulare cio' che varia,
preferire la composizione all'ereditarieta', dipendere dalle astrazioni. */
interface Notificatore { // l'astrazione da cui dipende il codice di alto livello
void invia(String destinatario, String testo);
}
class NotificaMail implements Notificatore {
public void invia(String d, String t) { System.out.println("mail a " + d + ": " + t); }
}
class NotificaSms implements Notificatore {
public void invia(String d, String t) { System.out.println("sms a " + d + ": " + t); }
}
/* PRIMA: la classe di alto livello dipende dalla classe concreta e la crea con new (violazione di DIP e di OCP). */
class SollecitoRigido {
private final NotificaMail mail = new NotificaMail();
void sollecita(String utente) { mail.invia(utente, "restituisci il libro"); }
}
/* DOPO: dipende dall'interfaccia, che riceve da fuori (dependency injection): cambiare canale non tocca questa classe. */
class Sollecito {
private final Notificatore canale;
Sollecito(Notificatore canale) { this.canale = canale; }
void sollecita(String utente) { canale.invia(utente, "restituisci il libro"); }
}
/* Ereditarieta' contro composizione: una sottoclasse per ogni combinazione esplode (SollecitoMailConLog, SollecitoSmsConLog...).
Con la composizione il log e' un oggetto che si aggiunge a qualunque canale. */
class NotificaConLog implements Notificatore {
private final Notificatore interno;
NotificaConLog(Notificatore interno) { this.interno = interno; }
public void invia(String d, String t) { System.out.println("[log] invio a " + d); interno.invia(d, t); }
}
public class Principi {
public static void main(String[] args) {
new SollecitoRigido().sollecita("anna");
new Sollecito(new NotificaMail()).sollecita("anna");
new Sollecito(new NotificaSms()).sollecita("luca");
new Sollecito(new NotificaConLog(new NotificaSms())).sollecita("luca");
}
}mail a anna: restituisci il libro
mail a anna: restituisci il libro
sms a luca: restituisci il libro
[log] invio a luca
sms a luca: restituisci il libroCome scegliere e usare un pattern
- Partire dal problema, non dal pattern: quale parte del progetto è rigida, duplicata o difficile da cambiare? Quale aspetto deve poter variare (la classe creata, l'algoritmo, il formato, il numero dei dipendenti)?
- Cercare nel catalogo i pattern con intento corrispondente e confrontarli (per esempio Adapter e Facade hanno entrambi un'interfaccia nuova per classi esistenti ma intenti diversi).
- Leggere conseguenze e pattern correlati prima di decidere: ogni pattern costa complessità (classi in più, indirezione).
- Adattare il pattern al linguaggio (in Java le lambda sostituiscono Strategy e Command a un solo metodo;
IteratoreObserveresistono già nel JDK). - Non usarlo se il problema non c'è: un pattern applicato "per principio" a un programma semplice è over-engineering (il costo si paga nella manutenzione, vedi Ingegneria del software - prodotto software e processo di sviluppoL'ingegneria del software (IdS) è la disciplina che copre tutti gli aspetti della produzione di software professionale, dalle prime specifiche alla manutenzione: il software sono programmi più documentazione, può essere generico (venduto sul mercato) o specifico (su misura per un cliente), e un buon prodotto ha quattro attributi (manutenibilità, affidabilità e sicurezza, efficienza, accettabilità); le attività fondamentali sono specifica, sviluppo, validazione ed evoluzione; i costi sono dominati dalla manutenzione e un difetto di requisiti scoperto dopo il rilascio costa fino a 100 volte uno scoperto subito; i tre problemi di fondo sono incertezza, irreversibilità e complessità; verifica ("costruiamo bene il prodotto?") e validazione ("costruiamo il prodotto giusto?") sono cose diverse.Ingegneria del software - prodotto software e processo di sviluppo →).
Errori comuni: usare i pattern come fine (avere un Singleton "perché fa moderno"); confondere pattern e idiom (una soluzione specifica di un linguaggio, per esempio try-with-resources); confondere pattern e stile architetturale (stessa idea, scala diversa, vedi Architettura del software - decisioni, organizzazione e stiliL'architettura del software descrive come un sistema è organizzato in componenti, le loro relazioni e la loro distribuzione; è il ponte tra requisiti e progetto dettagliato, e le sue decisioni derivano soprattutto dai requisiti non funzionali (prestazioni, sicurezza, disponibilità, manutenibilità), spesso in conflitto tra loro; si descrive con più viste (logica, processi, sviluppo, fisica); gli stili di organizzazione ricorrenti sono a livelli (layered), MVC, repository, client-server, pipe and filter, publish-subscribe e microservizi; la modularizzazione mira ad alta coesione e basso accoppiamento, nascondendo le decisioni dietro interfacce (information hiding, inversione delle dipendenze).Architettura del software - decisioni, organizzazione e stili →); dimenticare che un pattern è un modello da adattare, non codice da copiare.
Progettare gli oggetti con le responsabilità
Larman inquadra i pattern nella progettazione guidata dalle responsabilità (responsibility-driven design): prima di scegliere un pattern GoF si decide chi fa che cosa. Gli ingredienti da cui si parte: testo dei casi d'uso, diagramma di sequenza di sistema, contratti delle operazioni, specifica supplementare (requisiti non funzionali), glossario, modello di dominio. Le responsabilità sono di due tipi (sapere e fare) e si assegnano con i principi GRASP (Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →); i modelli dinamici (diagrammi di interazione) aiutano a progettare la logica e sono i più difficili e importanti, i modelli statici (diagrammi delle classi) definiscono pacchetti, classi, attributi e firme. Contano le capacità di progettare oggetti, non quella di disegnare UML: il disegno è un riflesso delle decisioni.
Domande d'esame
1. Che cos'è un design pattern? Quali sono gli elementi essenziali e perché è utile avere un nome? Traccia. Soluzione generale, riusabile e collaudata a un problema ricorrente in un contesto. Elementi: nome, problema, soluzione (struttura e responsabilità, non implementazione), conseguenze. Il nome crea un vocabolario comune e permette di ragionare sul progetto senza entrare nei dettagli.
2. Classificare Singleton, Adapter, Observer, Factory Method, Decorator, Strategy nelle tre famiglie GoF e dire per ognuno che problema risolve. Traccia. Creazionali: Singleton (istanza unica), Factory Method (classe creata decisa dalle sottoclassi). Strutturali: Adapter (interfaccia incompatibile), Decorator (responsabilità aggiunte dinamicamente). Comportamentali: Observer (notifica dei cambiamenti di stato), Strategy (algoritmi intercambiabili).
3. Perché l'istruzione Duck d = new DecoyDuck(); viola un principio di progetto e come si rimedia?
Traccia. Anche con la variabile di tipo interfaccia, la classe dipende dalla classe concreta; con una scelta condizionale dipende da tutte e va modificata a ogni nuova classe (viola aperto/chiuso). Si isola la creazione in una fabbrica (Factory Method, Abstract Factory) e si inietta la dipendenza, così il codice di alto livello dipende solo dall'interfaccia (inversione delle dipendenze).
Versione ripasso
- Pattern: soluzione ricorrente a un problema standard in un contesto (Alexander); per il software, coppia nome-problema/soluzione applicabile in contesti nuovi (Larman). Riuso dell'esperienza di progetto; il nome dà un vocabolario.
- Quattro elementi: nome, problema (intento, contesto, applicabilità), soluzione (struttura, partecipanti, collaborazioni: un modello, non un'implementazione), conseguenze (compromessi).
- Schema GoF: intento, also known as, motivazione, applicabilità, struttura, partecipanti, conseguenze, implementazione, codice, usi noti, pattern correlati.
- Famiglie: GRASP (assegnare responsabilità), GoF (23 pattern, 1995), architetturali (Architettura del software - decisioni, organizzazione e stiliL'architettura del software descrive come un sistema è organizzato in componenti, le loro relazioni e la loro distribuzione; è il ponte tra requisiti e progetto dettagliato, e le sue decisioni derivano soprattutto dai requisiti non funzionali (prestazioni, sicurezza, disponibilità, manutenibilità), spesso in conflitto tra loro; si descrive con più viste (logica, processi, sviluppo, fisica); gli stili di organizzazione ricorrenti sono a livelli (layered), MVC, repository, client-server, pipe and filter, publish-subscribe e microservizi; la modularizzazione mira ad alta coesione e basso accoppiamento, nascondendo le decisioni dietro interfacce (information hiding, inversione delle dipendenze).Architettura del software - decisioni, organizzazione e stili →).
- Catalogo GoF:
- Creazionali (5): Singleton, Factory Method, Abstract Factory, Builder, Prototype.
- Strutturali (7): Adapter, Decorator, Composite, Facade, Proxy, Bridge, Flyweight.
- Comportamentali (11): Observer, Strategy, Command, Iterator, Template Method, Chain of Responsibility, Interpreter, Mediator, Memento, State, Visitor.
- Intenti da ricordare: Adapter converte un'interfaccia; Decorator aggiunge responsabilità; Composite tratta uniformemente parti e tutto; Facade semplifica un sottosistema; Proxy controlla l'accesso; Observer notifica i dipendenti; Strategy sceglie un algoritmo; Command rende la richiesta un oggetto (annullabile); Iterator scorre senza esporre la struttura; Template Method fissa lo scheletro; State cambia comportamento con lo stato; Visitor aggiunge operazioni a una struttura; Memento salva e ripristina lo stato; Mediator coordina i colleghi; Chain passa la richiesta lungo una catena; Bridge separa astrazione e implementazione; Flyweight condivide oggetti a grana fine; Interpreter valuta una piccola grammatica.
- UML (leggere le strutture): ereditarietà = triangolo vuoto (is-a); aggregazione = rombo vuoto; composizione = rombo pieno (parti muoiono col tutto); dipendenza = freccia tratteggiata.
- Principi: programmare verso un'interfaccia; incapsulare ciò che varia; composizione al posto dell'ereditarietà; aperto/chiuso; inversione delle dipendenze (alto e basso livello dipendono da astrazioni); responsabilità unica.
| principio | esempio |
|---|---|
| interfaccia | Notificatore canale, non NotificaMail |
| incapsulare ciò che varia | la scelta del canale (mail, SMS) fuori da Sollecito |
| composizione | NotificaConLog(new NotificaSms()) e non una sottoclasse per ogni coppia |
| aperto/chiuso | nuovo canale = nuova classe, nessuna modifica a Sollecito |
- Linee guida DIP: nessuna variabile verso classi concrete, nessuna classe derivata da una concreta, nessun override di metodi già implementati (sono linee guida: alla lettera non si compila).
- Il problema di
new:Duck d = new DecoyDuck()lega la classe alla classe concreta; unifche sceglie la classe dipende da tutte e viola aperto/chiuso. Rimedio: fabbriche (Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →) e dependency injection.
class Sollecito {
private final Notificatore canale;
Sollecito(Notificatore canale) { this.canale = canale; } // iniettato dall'esterno
void sollecita(String utente) { canale.invia(utente, "restituisci il libro"); }
}
new Sollecito(new NotificaConLog(new NotificaSms())).sollecita("luca"); // log + SMS- Come scegliere: partire dal problema, confrontare gli intenti, leggere le conseguenze, adattare al linguaggio (lambda per Strategy e Command), non usare un pattern se il problema non c'è.
- RDD: si progettano prima le responsabilità (sapere, fare) con Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →; i diagrammi di interazione (dinamici) sono i più importanti.
- Errori tipici: usare i pattern come fine (over-engineering); confondere pattern, idiom e stile architetturale; copiare il codice invece di adattare il modello; scegliere dal nome e non dall'intento (Adapter e Facade, Strategy e State).
Esercizi su questo argomento
Teoria collegata
- Architettura del software - decisioni, organizzazione e stili
- Ingegneria del software - prodotto software e processo di sviluppo
- Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method
- Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype
- Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy
- Principi GRASP per l'assegnazione delle responsabilità
- Processi di sviluppo - modelli plan-based, metodi agili e Scrum
- Software testing - progettazione dei casi di test e JUnit