Esercizio - Design pattern, elementi e classificazione (secondo appello 2022-23, teoria)
In questa pagina 4
Domanda aperta 2 del secondo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →, 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 →, 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à →. Qui l'esempio è in C++; nella parte di Ingegneria del Software gli stessi pattern sono in Java.
Testo. Definire il concetto di design pattern. Descriverne i quattro elementi essenziali e i criteri con cui sono classificati. Fare un esempio di design pattern descrivendo le condizioni che ne rendono opportuno l'uso.
Definizione
Design pattern. Una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, descritta in modo astratto: non è codice da copiare né una libreria, è un modello di classi e oggetti, delle loro responsabilità e collaborazioni, da adattare al caso. Il catalogo di riferimento è quello GoF (Gang of Four: Gamma, Helm, Johnson, Vlissides), con 23 pattern per il progetto orientato agli oggetti.
I quattro elementi essenziali
| elemento | contenuto |
|---|---|
| nome (pattern name) | un'etichetta di una o due parole: aumenta il vocabolario dei progettisti, permette di discutere un progetto a un livello più alto ("qui usiamo uno Strategy") senza entrare nei dettagli |
| problema (problem) | descrive quando applicare il pattern: l'intento, il contesto, i sintomi di un progetto rigido (condizioni da soddisfare) |
| soluzione (solution) | gli elementi del progetto: classi e oggetti, le relazioni, le responsabilità e le collaborazioni. È un modello astratto, non un progetto o un'implementazione concreti: si adatta al linguaggio |
| conseguenze (consequences) | risultati e compromessi dell'applicazione: spazio, tempo, flessibilità, quale aspetto del sistema si può variare in modo indipendente |
Come si classificano
I pattern GoF si classificano secondo due criteri.
- Scopo (purpose): che cosa fa il pattern.
| famiglia | che cosa descrive | esempi |
|---|---|---|
| creazionali (5) | come si creano gli oggetti | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| strutturali (7) | come si compongono classi e oggetti in strutture più grandi | Adapter, Decorator, Composite, Facade, Proxy, ... |
| comportamentali (11) | come collaborano gli oggetti e si ripartiscono le responsabilità | Observer, Strategy, Command, Iterator, Template Method, ... |
- Ambito (scope): a chi si applica il pattern: alle classi (relazioni statiche fissate a compile-time, per ereditarietà, come Factory Method, Template Method e Adapter di classe) o agli oggetti (relazioni dinamiche, per composizione e modificabili a run-time, come Strategy, Observer, Decorator, Abstract Factory). La maggior parte dei pattern è a livello di oggetto.
Altri criteri utili nella pratica: il problema che risolvono, i pattern correlati (che si combinano o si scambiano), i principi che realizzano (programmare verso un'interfaccia, preferire la composizione all'ereditarietà, incapsulare ciò che varia).
Un esempio: Strategy (comportamentale, a livello di oggetto)
Nome: Strategy. Problema: una classe deve poter variare l'algoritmo che usa (per esempio come calcolare uno sconto, come ordinare, come comprimere) e non si vuole riempirla di if/else sul tipo di algoritmo, né creare una sottoclasse per ogni variante. Soluzione: si definisce una famiglia di algoritmi, ciascuno incapsulato in una classe con la stessa interfaccia (Sconto::applica); il contesto (Carrello) tiene un riferimento a una strategia e le delega il lavoro; la strategia si può cambiare a run-time. Conseguenze: (+) algoritmi intercambiabili, niente condizionali, nuovi algoritmi senza toccare il contesto (principio aperto/chiuso); (−) più classi e un'indirezione; il client deve conoscere le differenze fra le strategie.
Quando conviene usarlo: esistono molte varianti di un comportamento; si vuole scegliere l'algoritmo a run-time; un algoritmo ha dati che il client non deve vedere; si vedono lunghe catene di if/switch sul tipo di algoritmo. Quando no: una sola variante, o varianti che non cambiano mai (basta una funzione). In C++ moderno si può anche usare una lambda o std::function al posto delle classi.
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
// PATTERN STRATEGY: la famiglia di algoritmi (come calcolare lo sconto) e' incapsulata in classi
// intercambiabili con la stessa interfaccia; chi la usa non conosce quella concreta.
class Sconto { // interfaccia della strategia
public:
virtual ~Sconto() = default;
virtual double applica(double prezzo) const = 0;
};
class NessunoSconto : public Sconto {
public:
double applica(double prezzo) const override { return prezzo; }
};
class ScontoPercentuale : public Sconto {
public:
explicit ScontoPercentuale(double perc) : perc_{perc} {}
double applica(double prezzo) const override { return prezzo * (1 - perc_ / 100); }
private:
double perc_;
};
class ScontoFisso : public Sconto {
public:
explicit ScontoFisso(double euro) : euro_{euro} {}
double applica(double prezzo) const override { return prezzo > euro_ ? prezzo - euro_ : 0; }
private:
double euro_;
};
class Carrello { // il "contesto": usa una strategia
public:
explicit Carrello(unique_ptr<Sconto> s) : sconto_{std::move(s)} {}
void aggiungi(double prezzo) { totale_ += prezzo; }
void cambia_strategia(unique_ptr<Sconto> s) { sconto_ = std::move(s); } // si cambia a run-time
double da_pagare() const { return sconto_->applica(totale_); } // nessun if sul tipo di sconto
private:
unique_ptr<Sconto> sconto_;
double totale_ = 0;
};
int main()
{
Carrello c(make_unique<NessunoSconto>());
c.aggiungi(60);
c.aggiungi(40);
cout << "senza sconto: " << c.da_pagare() << '\n';
c.cambia_strategia(make_unique<ScontoPercentuale>(10));
cout << "sconto del 10%: " << c.da_pagare() << '\n';
c.cambia_strategia(make_unique<ScontoFisso>(25));
cout << "sconto di 25: " << c.da_pagare() << '\n';
return 0;
}senza sconto: 100
sconto del 10%: 90
sconto di 25: 75Scontoè l'interfaccia della strategia;NessunoSconto,ScontoPercentuale,ScontoFissosono le strategie concrete;Carrelloè il contesto e non sa quale sconto sta usando.cambia_strategia(...)sostituisce l'algoritmo a run-time, senzaifnelCarrello. Con 100 di spesa: nessuno sconto 100; 90; sconto fisso di 25 75.- È polimorfismo dinamico usato per delegare un comportamento: ereditarietà per l'interfaccia, composizione per il contesto (C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →).
Versione ripasso
- Design pattern = soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione; un modello di classi/oggetti e responsabilità, non codice da copiare. Catalogo GoF: 23 pattern. Rimando: Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →.
- Quattro elementi: nome (vocabolario, discutere a un livello alto), problema (quando applicarlo: intento, contesto, sintomi), soluzione (classi, oggetti, relazioni, responsabilità: modello astratto), conseguenze (risultati e compromessi: spazio, tempo, flessibilità).
- Classificazione: per scopo (creazionali: come si creano gli oggetti; strutturali: come si compongono classi e oggetti; comportamentali: come collaborano e si ripartiscono le responsabilità) e per ambito (a livello di classe, per ereditarietà e fisso in compilazione; a livello di oggetto, per composizione e variabile a run-time).
- Esempio, Strategy (comportamentale, oggetto): famiglia di algoritmi incapsulati in classi con la stessa interfaccia, intercambiabili; il contesto delega. Si usa con molte varianti di un comportamento, scelta a run-time, lunghe catene di
if/switch. Conseguenze: algoritmi intercambiabili e aperto/chiuso, ma più classi e un'indirezione.
class Sconto { public: virtual ~Sconto() = default; virtual double applica(double prezzo) const = 0; };
class ScontoPercentuale : public Sconto { /* applica: prezzo * (1 - perc / 100) */ };
class Carrello { unique_ptr<Sconto> sconto_;
public: void cambia_strategia(unique_ptr<Sconto> s) { sconto_ = std::move(s); }
double da_pagare() const { return sconto_->applica(totale_); } /* ... */ };Con 100 di spesa: nessuno sconto 100; 90; sconto fisso di 25 75.
- Errori tipici: descrivere un pattern come codice da copiare; confondere le famiglie (Singleton è creazionale, Adapter strutturale, Observer comportamentale); dimenticare le conseguenze; usare un pattern senza che il problema ci sia.