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