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
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:
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 (inviacontroinviaSms), parametri (un prefisso e una priorità in più, che l'adapter fornisce), tipo del risultato (booleancontroint). 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 interfacciaNotificatore, 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
/** 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
falseLettura 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)int) e interfaccia imposta (Notificatore.invia(utente, messaggio)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; AdapteeGatewaySmsLegacy; Adapter/ConcreteComponentNotificatoreSms; DecoratorNotificatoreDecorato; ConcreteDecoratorConRiprova,ConRegistro.
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 esserefinal. - 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.