Salta al contenuto
Note per Studenti Esercizio - Adapter e Decorator per le notifiche con un gateway SMS (progetto originale)

Esercizio - Adapter e Decorator per le notifiche con un gateway SMS (progetto originale)

Esercizio originale, nello stile del progetto d'esame (scelta e applicazione di pattern strutturali, con il codice). Teoria: Pattern strutturali - Adapter, Decorator, Composite, Facade e ProxyI pattern strutturali compongono classi e oggetti in strutture più grandi: Adapter converte l'interfaccia di una classe in quella attesa dal client (di oggetto, per composizione, o di classe, per ereditarietà multipla di classe e interfaccia); Decorator aggiunge responsabilità a un oggetto con un involucro che ha la stessa interfaccia (l'ordine dei decoratori conta); Composite tratta in modo uniforme foglie e contenitori in una struttura ad albero; Facade offre un'interfaccia semplice a un sottosistema (non cerca il polimorfismo, a differenza dell'Adapter); Proxy è un sostituto con la stessa interfaccia che controlla l'accesso (virtuale, di protezione, di cache, dinamico); Bridge e Flyweight completano le sette forme del catalogo.Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy →. Il codice è stato compilato ed eseguito con JDK 21.

Testo

L'applicazione della biblioteca usa l'interfaccia

java
interface Notificatore {
    boolean invia(String utente, String messaggio);   // true se consegnato
}

per avvisare gli iscritti. Per gli SMS il comune fornisce una libreria di terzi, di cui non si può modificare il codice:

java
class GatewaySmsLegacy {
    int inviaSms(String numero, String testo, int priorita);   // restituisce 0 se va bene, altrimenti un codice d'errore
}

Requisiti: (a) l'applicazione deve inviare SMS senza conoscere il gateway, con il prefisso internazionale +39- e priorità fissa 5; (b) il gateway fallisce spesso, quindi l'invio deve essere ritentato fino a un numero massimo di volte; (c) ogni invio deve essere registrato (consegnato o fallito); (d) le funzioni (b) e (c) devono potersi usare in qualunque combinazione e con qualunque altro canale futuro (mail), non solo con gli SMS.

Si richiede di: 1) scegliere e motivare i pattern per (a) e per (b)-(d); 2) indicare i ruoli; 3) scrivere il codice con un main che provi un gateway che fallisce due volte, con 3 e con 2 tentativi; 4) dire se l'Adapter qui è di classe o di oggetto e perché.

Soluzione

1. Scelta

  • (a) Adapter. Esiste una classe che non si può modificare (GatewaySmsLegacy) e un'interfaccia imposta dal resto dell'applicazione (Notificatore): l'interfaccia è diversa per nomi (invia contro inviaSms), parametri (un prefisso e una priorità in più, che l'adapter fornisce), tipo del risultato (boolean contro int). Non è una Facade (non si semplifica un sottosistema, si rispetta un'interfaccia) né un Proxy (non si controlla l'accesso).
  • (b)-(d) Decorator. Riprovare e registrare sono responsabilità aggiuntive allo stesso comportamento di base, combinabili e ripetibili su canali diversi. Con le sottoclassi servirebbe una classe per ogni combinazione e per ogni canale (SmsConRiprova, SmsConRegistro, SmsConRiprovaERegistro, MailConRiprova...); con i decoratori ogni responsabilità è una classe, con la stessa interfaccia Notificatore, che ne contiene un'altra.

2. Ruoli

pattern ruolo classe
Adapter Target Notificatore
Adaptee GatewaySmsLegacy
Adapter NotificatoreSms
Client il codice dell'applicazione (main)
Decorator Component Notificatore
ConcreteComponent NotificatoreSms
Decorator NotificatoreDecorato
ConcreteDecorator ConRiprova, ConRegistro

3. Codice

java
/** Target: cio' che il resto dell'applicazione chiama. */
interface Notificatore {
    boolean invia(String utente, String messaggio);
}

/** Adaptee: libreria SMS di terzi, interfaccia diversa e non modificabile. */
class GatewaySmsLegacy {
    private int tentativi = 0;
    private final int fallimentiIniziali;
    GatewaySmsLegacy(int fallimentiIniziali) { this.fallimentiIniziali = fallimentiIniziali; }

    /** Restituisce 0 se va a buon fine, un codice di errore diverso da 0 altrimenti. */
    int inviaSms(String numero, String testo, int priorita) {
        tentativi++;
        System.out.println("    gateway: tentativo " + tentativi + " verso " + numero + " (priorita " + priorita + ")");
        return tentativi <= fallimentiIniziali ? 503 : 0;
    }
}

/** Adapter di oggetto: traduce nomi, tipo del risultato e parametri mancanti (priorita fissa). */
class NotificatoreSms implements Notificatore {
    private final GatewaySmsLegacy gateway;
    NotificatoreSms(GatewaySmsLegacy gateway) { this.gateway = gateway; }
    public boolean invia(String utente, String messaggio) {
        return gateway.inviaSms("+39-" + utente, messaggio, 5) == 0;
    }
}

/** Decorator base. */
abstract class NotificatoreDecorato implements Notificatore {
    protected final Notificatore interno;
    NotificatoreDecorato(Notificatore interno) { this.interno = interno; }
}

class ConRiprova extends NotificatoreDecorato {
    private final int massimo;
    ConRiprova(Notificatore interno, int massimo) { super(interno); this.massimo = massimo; }
    public boolean invia(String utente, String messaggio) {
        for (int i = 1; i <= massimo; i++) {
            if (interno.invia(utente, messaggio)) return true;
            System.out.println("  riprova " + i + "/" + massimo);
        }
        return false;
    }
}

class ConRegistro extends NotificatoreDecorato {
    ConRegistro(Notificatore interno) { super(interno); }
    public boolean invia(String utente, String messaggio) {
        boolean ok = interno.invia(utente, messaggio);
        System.out.println("  [registro] " + utente + " -> " + (ok ? "consegnato" : "FALLITO"));
        return ok;
    }
}

public class Gateway {
    public static void main(String[] args) {
        // il gateway fallisce le prime 2 volte: con 3 tentativi riesce, con 2 no
        Notificatore tre = new ConRegistro(new ConRiprova(new NotificatoreSms(new GatewaySmsLegacy(2)), 3));
        System.out.println(tre.invia("3331234567", "restituisci il libro"));
        Notificatore due = new ConRegistro(new ConRiprova(new NotificatoreSms(new GatewaySmsLegacy(2)), 2));
        System.out.println(due.invia("3331234567", "restituisci il libro"));
    }
}

Uscita:

    gateway: tentativo 1 verso +39-3331234567 (priorita 5)
  riprova 1/3
    gateway: tentativo 2 verso +39-3331234567 (priorita 5)
  riprova 2/3
    gateway: tentativo 3 verso +39-3331234567 (priorita 5)
  [registro] 3331234567 -> consegnato
true
    gateway: tentativo 1 verso +39-3331234567 (priorita 5)
  riprova 1/2
    gateway: tentativo 2 verso +39-3331234567 (priorita 5)
  riprova 2/2
  [registro] 3331234567 -> FALLITO
false

Lettura dell'uscita: con 3 tentativi e un gateway che fallisce le prime due volte, il terzo tentativo riesce (true, registro "consegnato"); con 2 tentativi ConRiprova esaurisce i tentativi e restituisce false (registro "FALLITO"). L'ordine dei decoratori conta: ConRegistro(ConRiprova(sms)) registra una sola volta l'esito finale; ConRiprova(ConRegistro(sms)) registrerebbe ogni tentativo.

4. Adapter di oggetto, non di classe

NotificatoreSms contiene un GatewaySmsLegacy (composizione): è un adapter di oggetto. Si preferisce perché (1) il gateway può essere passato nel costruttore e quindi sostituito con un finto nei test (qui un gateway che fallisce un numero fisso di volte); (2) lo stesso adapter servirebbe anche una sottoclasse del gateway; (3) un adapter di classe (extends GatewaySmsLegacy implements Notificatore) esporrebbe a chiunque usi il tipo concreto anche inviaSms, rompendo l'interfaccia scelta; inoltre il gateway di terzi potrebbe essere final o non avere un costruttore accessibile.

Versione ripasso

  • (a) Adapter: classe non modificabile (GatewaySmsLegacy.inviaSms(numero, testo, priorita) →\to int) e interfaccia imposta (Notificatore.invia(utente, messaggio) →\to boolean): cambiano nome, parametri (prefisso +39- e priorità 5 forniti dall'adapter) e tipo del risultato. Non Facade (non semplifica), non Proxy (non controlla l'accesso).
  • (b)-(d) Decorator: riprovare e registrare = responsabilità aggiuntive, combinabili, valide per ogni canale; con le sottoclassi servirebbe una classe per ogni combinazione e canale.
  • Ruoli: Target/Component Notificatore; Adaptee GatewaySmsLegacy; Adapter/ConcreteComponent NotificatoreSms; Decorator NotificatoreDecorato; ConcreteDecorator ConRiprova, ConRegistro.
java
class NotificatoreSms implements Notificatore {                       // Adapter di oggetto
    private final GatewaySmsLegacy gateway;
    NotificatoreSms(GatewaySmsLegacy g) { this.gateway = g; }
    public boolean invia(String utente, String messaggio) {
        return gateway.inviaSms("+39-" + utente, messaggio, 5) == 0;   // traduce parametri e risultato
    }
}
class ConRiprova extends NotificatoreDecorato {
    private final int massimo;
    ConRiprova(Notificatore interno, int massimo) { super(interno); this.massimo = massimo; }
    public boolean invia(String utente, String messaggio) {
        for (int i = 1; i <= massimo; i++) if (interno.invia(utente, messaggio)) return true;
        return false;
    }
}
Notificatore tre = new ConRegistro(new ConRiprova(new NotificatoreSms(new GatewaySmsLegacy(2)), 3));
  • Uscita: gateway che fallisce 2 volte: con 3 tentativi consegna (terzo tentativo, [registro] ... consegnato, true); con 2 tentativi fallisce ([registro] ... FALLITO, false).
  • Ordine dei decoratori: ConRegistro(ConRiprova(...)) registra una volta l'esito finale; ConRiprova(ConRegistro(...)) registrerebbe ogni tentativo.
  • Adapter di oggetto, non di classe: gateway iniettabile (finto nei test), serve anche sottoclassi, non espone inviaSms; il gateway di terzi potrebbe essere final.
  • Errori tipici: adapter che modifica la libreria; una sottoclasse per ogni combinazione di funzioni; decorare senza pensare all'ordine; adapter di classe che espone i metodi dell'adaptee.

Teoria collegata