Salta al contenuto
Note per Studenti Esercizio - Design pattern, elementi e classificazione (secondo appello 2022-23, teoria)

Esercizio - Design pattern, elementi e classificazione (secondo appello 2022-23, teoria)

Esame
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.

  1. 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, ...
  1. 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.

strategy.cppcpp
#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:   75

Versione ripasso

cpp
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 →\to 100; 10%10\% →\to 90; sconto fisso di 25 →\to 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.

Esercizi su questo argomento

Teoria collegata