Ingegneria del Software - Observer, Composite, Strategy, Command

Descrizione dei design pattern Observer, Composite, Strategy e Command.

Observer

L'observer è un design pattern comportamentale.

Definisce una dipendenza di tipo uno-a-molti tra oggetti in modo che quando un oggetto cambia lo stato, tutti i suoi oggetti dipendenti vengono notificati e aggiornati automaticamente.

Problema e soluzione

  • Notificare le modifiche ogni volta che un oggetto cambia il proprio stato
  • Le notifiche dovrebbero coinvolgere solo gli oggetti interessati
  • Deve essere possibile aggiungere o rimuovere gli osservatori in qualsiasi momento

  • Integrare un meccanismo di iscrizione nel subject
  • Il subject si occupa di notificare tutti gli oggetti iscritti quando cambia lo stato
  • L'accoppiamento tra subject e observer deve essere lasco

UML

Loading diagram...

Diagramma di sequenza

Loading diagram...

Codice Subject

public abstract class Subject {
    protected Set<Observer> observers;
    public void attach(Observer observer) { observers.put(observer); }
    public void detach(Observer observer) { observers.remove(observer); }
    protected void notify(Object state) {
        for (Observer observer : observers)
            observer.update(this, state);
    }
}
public class BookStore extends Subject {
    List<Book> books;
    public List<Book> getBooks() { return books; }
    public void addBook(Book book) {
        books.add(book);
        notify(books);
    }
}

Codice Observer

public interface Observer {
    public void update(Subject subject, Object state);
}
public class Reader implements Observer {
    List<String> bookWishlist;

    @Override
    public void update(Subject subject, Object state){
        if (state instanceof List<Book> books){
            for (Book book : books) {
                if (wishlist.contains(book))
                    book.buy();
            }
        }
    }
}

Implementazione in Java

Data l'importanza del pattern, Java ne fornisce una implementazione nativa, con le interfacce Observer e Observable. Tuttavia il loro utilizzo è stato deprecato a partire dalla versione 9.

L'alternativa nativa è rappresentata da java.util.concurrent.Flow, che fornisce un'interfaccia per la pubblicazione e sottoscrizione di eventi in maniera asincrona.

Possibili applicazioni

  • Gestione di un'architettura distribuita
  • Sistema di notifiche
  • Attesa di input esterni

Pro e contro

  • La lista di oggetti da notificare è dinamica
  • Approccio push invece che pull
  • Associazione lasca fra Subject e Observers

  • Non c'è garanzia nell'ordine delle notifiche
  • L'interfaccia dell'Observer tende a essere generica e necessitare cast

Composite

Il composite è un design pattern strutturale.

Consente di rappresentare oggetti composti da altri oggetti in modo da trattarli come se fossero oggetti semplici.

Problema e soluzione

  • Necessità di gestire una gerarchia di oggetti ad albero
  • Evitare che il client si preoccupi di gestire interfacce diverse per oggetti semplici e composti
  • Le chiamate devono essere riportate a tutti gli oggetti della struttura

  • Implementare una singola interfaccia per tutti gli oggetti della gerarchia
  • Permettere una specializzazione, trasparente per il client, di oggetti semplici e composti
  • I metodi possono essere richiamati ricorsivamente

UML

Loading diagram...

Codice

public interface Item {
    public float getCost();
}
public class Book implements Item {
    private float price;
    public float getCost() { return price; }
}
public class Box implements Item {
    private List<Item> items;
    public void add(Item item) { items.add(item); }
    public void remove(Item item) { items.remove(item); }
    public float getCost() {
        float cost = 0;
        for (Item item : items)
            cost += item.getCost();
        return cost;
    }
}

Possibili applicazioni

  • Strutture ad alberi
  • Gestione di file e cartelle
  • Organizzazione di oggetti in una gerarchia

Pro e contro

  • Il client non deve conoscere la gerarchia
  • È possibile interagire con gli oggetti utilizzando la medesima interfaccia

  • Non è applicabile se le differenze fra le categorie di oggetti iniziano a diventare troppo evidenti

Strategy

Il strategy è un design pattern comportamentale.

Consente di definire una famiglia di algoritmi, rendendoli intercambiabili e indipendenti dagli altri.

Problema e soluzione

  • Necessità di utilizzare diversi algoritmi per lo stesso scopo
  • Fare in modo che l'implementazione sia trasparente al client
  • Poter cambiare l'algoritmo a runtime

  • Definire una interfaccia comune per tutti gli algoritmi
  • Separare l'algoritmo dalla classe che lo utilizzerà
  • Applicare dependency injection

UML e diagramma di sequenza

Loading diagram...

Loading diagram...

Codice

public class Graph {
    // ...
    SPAlgorithm algorithm;
    public void setShortestPathAlgorithm(SPAlgorithm algorithm) {
        this.algorithm = algorithm;
    }
    public List<Node> shortestPath(Node source, Node destination) {
        return algorithm.shortestPath(source, destination);
    }
}
public class Dijkstra implements SPAlgorithm {
    public List<Node> shortestPath(Node source, Node destination) {
        // ...
    }
}

public class BellmanFord implements SPAlgorithm {
    public List<Node> shortestPath(Node source, Node destination) {
        // ...
    }
}

Possibili applicazioni

  • Gestione di algoritmi di ordinamento
  • Diverse metodologie di calcolo di un percorso
  • Possibilità di selezione fra algoritmi più o meno efficienti a seconda delle circostanze

Pro e contro

  • Possibilità di cambiare l'algoritmo a runtime
  • Separazione di responsabilità fra classe e algoritmo

  • I client potrebbero avere la necessità di conoscere gli algoritmi
  • Potrebbe essere sostituito da un approccio funzionale

Command

Il command è un design pattern comportamentale.

Prevede di incapsulare una richiesta in un oggetto, permettendo di parametrizzare uno stesso invoker con richieste diverse, impostando arbitrariamente il receiver della determinata richiesta.

Problema e soluzione

  • Mantenere separata la logica di presentazione da quella di business
  • Permettere ad uno stesso invoker di lanciare richieste diverse senza preoccuparsi della loro implementazione
  • Riutilizzare l'implementazione di una richiesta in più contesti

  • Incapsulare la richiesta stessa in un oggetto
  • Parametrizzare l'invoker con un oggetto che implementa l'interfaccia della richiesta
  • Far conoscere alla richiesta il receiver in grado di eseguirla

UML

Loading diagram...

Diagramma di sequenza

Loading diagram...

Invoker e receiver

// Invoker
public class Button {
    Command command;
    public void setCommand(Command command) {
        this.command = command;
    }
    public void click() {
        command.execute();
    }
}
// Receiver
public class DeathStar {
    private int x, y;
    public void selfDestroy() {
        System.out.println("La Death Star si è autodistrutta. But why?");
    }
    public void fireLaser() {
        System.out.println("Si spara il laser nel punto (" + x + ", " + y + ")");
    }
    public void aim(int x, int y) {
        this.x = x; this.y = y;
    }
}

Command

public interface Command {
    void execute();
}
public class SelfDestructCommand implements Command {
    private DeathStar deathStar;
    public SelfDestructCommand(DeathStar deathStar) {
        this.deathStar = deathStar;
    }
    public void execute() { deathStar.selfDestroy(); }
}
public class ShootCommand implements Command {
    private DeathStar deathStar;
    private int x, y;
    public ShootCommand(DeathStar deathStar, int x, int y) {
        this.deathStar = deathStar;
        this.x = x;
        this.y = y;
    }
    public void execute() { deathStar.aim(x, y); deathStar.fireLaser(); }
}

Possibili applicazioni

  • Comandi invocati da più sorgenti (GUI, CLI, etc.)
  • Trasferimento di un comando su un altro thread
  • Implementazione di un undo/redo
  • Scheduling dei comandi

Pro e contro

  • I comandi possono essere facilmente riutilizzati da più sender (invoker)
  • Separazione di responsabilità fra sender e receiver
  • Possibilità di comporre una sequenza di comandi

  • Aggiunta di un layer in più fra sender (invoker) e receiver

Challenge

  • (Observer) Implementare un sistema di notifiche per un sistema di messaggistica istantanea
  • (Composite) Simulare un file system con la possibilità di ottenere informazioni come la memoria occupata
  • (Strategy) Implementare un sistema di ordinamento di array di interi con diverse strategie (Bubble Sort, Merge Sort, Quick Sort, etc.)