Design patterns: PART 1 – Must have.

Quando si parla di Design Patterns, spesso si finisce per fare un elenco infinito di nomi: Factory, Strategy, Observer, Visitor, Decorator, Adapter, Command…

Ma conoscere un pattern non significa ricordarne il diagramma UML.
Significa riconoscere un problema ricorrente di progettazione e sapere quale struttura può risolverlo in modo elegante.

1. Dependency Inversion Principle

Partiamo dalla D di SOLID.

Il Dependency Inversion Principle dice, semplificando:

I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello. Entrambi dovrebbero dipendere da astrazioni.

E soprattutto:

Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni.

Un esempio classico:

// DIPENDENZA

class MySQLDatabase { 
    void save(String data) { 
       // ... 
     } 
} 

// UTILIZZATORE:

class UserService { 
     private MySQLDatabase database; 
     UserService() { 
          this.database = new MySQLDatabase(); 
     } 
}

Qui UserService dipende direttamente da MySQLDatabase.
Se domani voglio usare PostgreSQL, MongoDB o un database fake per i test, devo modificare UserService.

Con Dependency Inversion:

// ASTRAZIONE

interface Database { 
    void save(String data); 
} 

// IMPLEMENTAZIONE - DIPENDENZA

class MySQLDatabase implements Database { 
     public void save(String data) { // ... 
     } 
} 

// UTILIZZATORE

class UserService { 
     private final Database database; 

     UserService(Database database) { 
          this.database = database; 
       } 
}

Ora UserService conosce solo l’astrazione Database.
Il punto fondamentale non è quindi semplicemente “usiamo un’interfaccia”.

Il punto è:

la direzione della dipendenza è cambiata.

Il business non dipende dai dettagli infrastrutturali.

2. Chain of Responsibility

Un altro pattern estremamente interessante è la Chain of Responsibility.

una richiesta attraversa una catena di componenti, e ogni componente decide se gestirla oppure passarla al successivo.

Ci sono almeno due implementazioni che vale la pena distinguere.

Variante 1: chain esplicita

Ogni handler conosce il successivo.

abstract class Handler {
    protected Handler next;

    Handler next(Handler next) {
        this.next = next;
        return next;
    }

    abstract void handle(Request request);
}

Possiamo costruire:

handler1
    .next(handler2)
    .next(handler3);

E l'esecuzione diventa concettualmente:

Handler 1 -> Handler 2 -> Handler 3

Questa struttura è molto elegante quando vogliamo costruire dinamicamente una pipeline.

Variante 2: chain gestita da un ciclo

Un’altra possibilità è avere una collezione di handler:

List<Handler> handlers = List.of( authenticationHandler, authorizationHandler, validationHandler ); 

for (Handler handler : handlers) { 
     handler.handle(request); 
 }

In questo caso il “next” non è mantenuto dagli oggetti.

È il chiamante che controlla la catena. Questa distinzione è interessante perché cambia una cosa fondamentale:

chi possiede il controllo del flusso?

Nel primo caso la responsabilità è distribuita nella chain. Nel secondo caso è il chiamante a orchestrare la pipeline. La stessa idea può quindi avere due implementazioni architetturalmente molto diverse.

3. Observer

Se dovessi scegliere un pattern particolarmente importante per comprendere la programmazione reattiva, sceglierei sicuramente Observer.

un oggetto cambia stato e notifica automaticamente tutti gli interessati.

Esempio:

class Subject {

    private final List<Observer> observers = new ArrayList<>();

    void subscribe(Observer observer) {
        observers.add(observer);
    }

    void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

Quando succede qualcosa:

subject.notifyObservers("OrderCreated");

tutti gli observer ricevono l'evento.

Questa idea è alla base di moltissimi meccanismi moderni:

  • event-driven architecture
  • reactive programming
  • UI frameworks
  • event bus
  • streams
  • sistemi di notifica

In altre parole, Observer è uno dei mattoni concettuali che permettono di passare da:

“fai questa cosa”

a:

“quando succede questa cosa,
chi è interessato reagisca”

Ed è un cambiamento architetturale enorme.

4. Visitor

Il Visitor è probabilmente uno dei pattern più fraintesi.

Il problema che cerca di risolvere è particolare:

abbiamo una struttura di oggetti stabile, ma vogliamo poter aggiungere nuove operazioni senza modificare continuamente gli oggetti che compongono la struttura.

Per esempio:

interface Element {
    void accept(Visitor visitor);
}

class Circle implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Rectangle implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

Il Visitor:

interface Visitor {
    void visit(Circle circle);
    void visit(Rectangle rectangle);
}

Ora possiamo avere:

class AreaVisitor implements Visitor {

    public void visit(Circle circle) {
        // calcola area
    }

    public void visit(Rectangle rectangle) {
        // calcola area
    }
}

E domani:

class ExportVisitor implements Visitor {
    // esportazione
}

senza aggiungere queste operazioni direttamente a Circle e Rectangle.

Visitor è particolarmente interessante quando abbiamo:

molti tipi di elementi + molte operazioni diverse su quegli elementi.

Se invece la struttura cambia continuamente e le operazioni sono poche, può diventare una scelta poco conveniente.

5. MVVM

Infine, un pattern architetturale che considero molto interessante come alternativa a MVC:

MVVM — Model View ViewModel.

L’idea è separare la UI dalla logica di presentazione.

View -> ViewModel -> Model

La View non deve necessariamente conoscere direttamente la logica applicativa.

Per esempio:

class UserViewModel {

    private final UserService service;

    UserViewModel(UserService service) {
        this.service = service;
    }

    String getUserName() {
        return service.getCurrentUser().name();
    }
}

La View osserva o utilizza il ViewModel:

viewModel.getUserName();

Il vero punto di forza emerge quando introduciamo il paradigma reattivo (Observer):
Model -> ViewModel -> Observable State -> View

La View diventa quasi una funzione dello stato:

UI = f(state)

Quando lo stato cambia, la UI reagisce.

È una filosofia molto vicina a quella utilizzata da molti framework moderni.


Una cosa che accomuna tutti i design pattern

Disaccoppiare i componenti software.

  • Dependency Inversion riduce la dipendenza dai dettagli.
  • Chain of Responsibility riduce la dipendenza tra chi invia una richiesta e chi la gestisce.
  • Observer riduce la dipendenza diretta tra producer e consumer.
  • Visitor separa le operazioni dalla struttura degli oggetti.
  • MVVM separa la View dalla logica di presentazione.

Per garantire sopravvivenza e stabilità al software è fondamentale modularizzare e disaccoppiare il piu possibile le varie parti.

In questo modo al variare di una non siamo costretti al dover adattare tutto il resto.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *