Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy
In questa pagina 7
Definizione. I pattern strutturali riguardano la composizione di classi e oggetti per formare strutture più grandi o realizzare nuove funzioni, disaccoppiando interfaccia e implementazione. Il catalogo ne ha sette (Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →): qui sono trattati con esempio completo Adapter, Decorator, Composite, Facade e Proxy; Bridge e Flyweight sono descritti in fondo. Tutti i programmi sono compilati ed eseguiti con JDK 21, sul dominio biblioteca.
| pattern | in una riga | cosa cambia nell'interfaccia |
|---|---|---|
| Adapter | rende compatibile una classe esistente | nuova interfaccia (quella attesa dal client) |
| Decorator | aggiunge responsabilità a un oggetto | stessa interfaccia |
| Composite | oggetti singoli e composti trattati allo stesso modo | stessa interfaccia per foglie e contenitori |
| Facade | semplifica un sottosistema | nuova, più semplice |
| Proxy | controlla l'accesso a un oggetto | stessa interfaccia |
Adapter
Intento. Convertire l'interfaccia di una classe in un'altra interfaccia che i client si aspettano. L'Adapter permette a classi con interfacce incompatibili di lavorare insieme. Also known as: Wrapper. Come l'adattatore di una presa elettrica: rende compatibili una sorgente e un richiedente che non lo sono per natura.
Quando. Si vuole usare una classe esistente la cui interfaccia non corrisponde a quella che serve; si vuole creare una classe riusabile che collabori con classi non correlate dalle interfacce incompatibili. Tipico: una libreria di terzi o un sistema vecchio, di cui non si può (o non conviene) modificare il codice, e il resto dell'applicazione che si aspetta un'altra interfaccia (per esempio per mantenere il comportamento polimorfico).
| ruolo | nell'esempio della biblioteca | responsabilità |
|---|---|---|
| Target | RicercaCatalogo |
l'interfaccia che il client usa |
| Adaptee | CatalogoLegacy |
la classe esistente con l'interfaccia da adattare |
| Adapter | AdapterOggetto, AdapterClasse |
adatta l'Adaptee al Target |
| Client | Sportello |
usa solo il Target |
Adapter di oggetto e di classe
| adapter di oggetto | adapter di classe | |
|---|---|---|
| tecnica | composizione: l'Adapter ha un Adaptee (campo) | ereditarietà: l'Adapter estende l'Adaptee e implementa il Target |
| legame con l'Adaptee | un Adapter può servire molti Adaptee diversi, anche sottoclassi | legato a una classe concreta; le sottoclassi sconosciute possono dare problemi |
| ridefinire il comportamento | richiede sottoclassi dell'Adaptee e di farvi riferimento | basta ridefinire i metodi dell'Adaptee |
| esposizione | l'Adaptee resta nascosto | i metodi dell'Adaptee restano visibili a chi usa il tipo concreto dell'Adapter |
| in Java | sempre possibile | solo se il Target è un'interfaccia (una sola classe base) |
Quanto adattamento? Può essere una semplice traduzione di nomi e ordine dei parametri o un insieme di operazioni totalmente diverso (come qui: stringa unica separata da ; contro lista di titoli, maiuscole/minuscole, parametri mancanti). Un adapter bidirezionale supporta sia l'interfaccia Target sia quella dell'Adaptee, e si può usare al posto dell'uno o dell'altro.
import java.util.ArrayList;
import java.util.List;
/** Target: l'interfaccia che il client della biblioteca si aspetta. */
interface RicercaCatalogo {
List<String> cercaTitoli(String parolaChiave);
}
/** Adaptee: un vecchio catalogo che non possiamo modificare (niente sorgenti, o non e' nostro). */
class CatalogoLegacy {
private final String[] titoli = {"Il nome della rosa", "Il pendolo di Foucault", "La rosa purpurea del Cairo"};
/** Restituisce un array separato da ';' e fa distinzione tra maiuscole e minuscole. */
public String trova(String testo) {
StringBuilder sb = new StringBuilder();
for (String t : titoli) if (t.contains(testo)) sb.append(sb.length() == 0 ? "" : ";").append(t);
return sb.toString();
}
}
/** Adapter di OGGETTO: contiene l'adaptee (composizione). Puo' adattare anche sottoclassi di CatalogoLegacy. */
class AdapterOggetto implements RicercaCatalogo {
private final CatalogoLegacy adaptee;
AdapterOggetto(CatalogoLegacy adaptee) { this.adaptee = adaptee; }
public List<String> cercaTitoli(String parolaChiave) {
String grezzo = adaptee.trova(parolaChiave);
List<String> out = new ArrayList<>();
if (!grezzo.isEmpty()) for (String t : grezzo.split(";")) out.add(t);
return out;
}
}
/** Adapter di CLASSE: eredita l'adaptee (extends) e implementa il target (implements). */
class AdapterClasse extends CatalogoLegacy implements RicercaCatalogo {
public List<String> cercaTitoli(String parolaChiave) {
String grezzo = trova(parolaChiave); // chiama il metodo ereditato
List<String> out = new ArrayList<>();
if (!grezzo.isEmpty()) for (String t : grezzo.split(";")) out.add(t);
return out;
}
}
/** Client: conosce solo RicercaCatalogo. */
class Sportello {
static void stampa(RicercaCatalogo r, String q) {
System.out.println("'" + q + "' -> " + r.cercaTitoli(q));
}
}
public class AdapterEx {
public static void main(String[] args) {
RicercaCatalogo a = new AdapterOggetto(new CatalogoLegacy());
RicercaCatalogo b = new AdapterClasse();
Sportello.stampa(a, "rosa");
Sportello.stampa(b, "rosa");
Sportello.stampa(a, "xyz");
}
}'rosa' -> [Il nome della rosa, La rosa purpurea del Cairo]
'rosa' -> [Il nome della rosa, La rosa purpurea del Cairo]
'xyz' -> []Gli esempi del corso (CircQ, MyStack, MyStackClassAdapter, MyStackObjectAdapter) adattano una coda circolare a una pila: l'esercizio Esercizio - Adapter di classe e di oggetto per la pila (esempi del corso) li ricostruisce, corregge gli errori e li collauda con JUnit.
Errori nel materiale del corso. (1) Nella slide su
NumberSorter/SortListAdapteril metodosort(int[])è solo uno scheletro: converte un array in lista senza riempirla, chiama l'adaptee e restituiscesortedNumbers, mai dichiarato; non compila. Va riempita la lista con i valori dell'array e ricostruito l'array dalla lista restituita. (2)MainClassAdapterdegli esempi non compila: usa il tipoMyStack<String>per la variabile ma poi chiamalen()egetFromHead(), che l'interfaccia non ha, e fa un cast ((MyStackClassAdapter<String>)msca.toString()) applicato al risultato sbagliato; la variabile va dichiarata del tipo concreto o i metodi vanno limitati a quelli dell'interfaccia.
Adapter e Facade
| domanda | Facade | Adapter |
|---|---|---|
| ci sono classi preesistenti? | sì | sì |
| c'è un'interfaccia sulla quale devo progettare? | no | sì (quella del Target) |
| serve un comportamento polimorfico? | no | probabilmente |
| serve un'interfaccia più semplice? | sì | no (anche se più semplice sarebbe meglio, devo rispettare quella esistente) |
Decorator
Intento. Aggiungere responsabilità a un oggetto dinamicamente, in modo trasparente per il client: un'alternativa flessibile alle sottoclassi per estendere le funzioni.
Quando. Si vogliono aggiungere comportamenti a singoli oggetti a runtime senza toccare gli altri; l'estensione per sottoclassi darebbe un'esplosione di classi (una per ogni combinazione di opzioni: assicurazione, consegna, sconto...). Esempio nel JDK: gli stream di java.io (new BufferedReader(new InputStreamReader(System.in))).
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Component | Prestito |
interfaccia comune |
| ConcreteComponent | PrestitoBase |
l'oggetto da decorare |
| Decorator | PrestitoDecorato (astratta) |
ha lo stesso tipo del Component e contiene un Component; per default inoltra le chiamate |
| ConcreteDecorator | ConAssicurazione, ConConsegna, ScontoStudenti |
aggiungono comportamento prima o dopo aver inoltrato |
import java.util.Locale;
/** Component: tutto cio' che ha un costo di prestito. */
interface Prestito {
double costo();
String descrizione();
}
/** ConcreteComponent. */
class PrestitoBase implements Prestito {
private final String titolo;
PrestitoBase(String titolo) { this.titolo = titolo; }
public double costo() { return 1.00; }
public String descrizione() { return "prestito di '" + titolo + "'"; }
}
/** Decorator: ha lo stesso tipo del Component e CONTIENE un Component. */
abstract class PrestitoDecorato implements Prestito {
protected final Prestito interno;
PrestitoDecorato(Prestito interno) { this.interno = interno; }
public double costo() { return interno.costo(); } // per default inoltra
public String descrizione() { return interno.descrizione(); }
}
class ConAssicurazione extends PrestitoDecorato {
ConAssicurazione(Prestito p) { super(p); }
@Override public double costo() { return interno.costo() + 0.50; }
@Override public String descrizione() { return interno.descrizione() + " + assicurazione"; }
}
class ConConsegna extends PrestitoDecorato {
ConConsegna(Prestito p) { super(p); }
@Override public double costo() { return interno.costo() + 2.00; }
@Override public String descrizione() { return interno.descrizione() + " + consegna a domicilio"; }
}
class ScontoStudenti extends PrestitoDecorato {
ScontoStudenti(Prestito p) { super(p); }
@Override public double costo() { return interno.costo() * 0.5; } // l'ordine dei decoratori conta
@Override public String descrizione() { return interno.descrizione() + " - sconto studenti"; }
}
public class DecoratorEx {
static void stampa(Prestito p) {
System.out.println(String.format(Locale.ROOT, "%.2f %s", p.costo(), p.descrizione()));
}
public static void main(String[] args) {
Prestito base = new PrestitoBase("Il nome della rosa");
stampa(base);
stampa(new ConAssicurazione(base));
stampa(new ConConsegna(new ConAssicurazione(base)));
// sconto applicato prima o dopo la consegna: risultati diversi
stampa(new ConConsegna(new ScontoStudenti(base)));
stampa(new ScontoStudenti(new ConConsegna(base)));
// il decoratore del JDK piu' noto: tutte le classi di java.io
// new BufferedReader(new InputStreamReader(System.in))
}
}1.00 prestito di 'Il nome della rosa'
1.50 prestito di 'Il nome della rosa' + assicurazione
3.50 prestito di 'Il nome della rosa' + assicurazione + consegna a domicilio
2.50 prestito di 'Il nome della rosa' - sconto studenti + consegna a domicilio
1.50 prestito di 'Il nome della rosa' + consegna a domicilio - sconto studentiL'ordine dei decoratori cambia il risultato: sconto prima di consegna dà 2,50, dopo dà 1,50. Conseguenze: più flessibile delle sottoclassi e con classi piccole; ma molti piccoli oggetti, e l'identità dell'oggetto cambia (il decorato non è == all'originale).
Composite
Intento. Comporre oggetti in strutture ad albero per rappresentare gerarchie parte-tutto; permette ai client di trattare in modo uniforme gli oggetti singoli e le composizioni.
Quando. Si hanno gerarchie ricorsive (sezioni che contengono sezioni e documenti, cartelle e file, espressioni) e il client non deve distinguere foglie e contenitori.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Component | Voce |
interfaccia comune: nome, numeroDocumenti, valore, stampa |
| Leaf | Documento |
oggetto semplice, nessun figlio |
| Composite | Sezione |
contiene altre Voce e delega ricorsivamente il calcolo ai figli |
| Client | main |
usa Voce |
import java.util.ArrayList;
import java.util.List;
/** Component: foglie e contenitori si usano nello stesso modo. */
interface Voce {
String nome();
int numeroDocumenti();
double valore();
void stampa(String rientro);
}
/** Leaf. */
class Documento implements Voce {
private final String titolo;
private final double prezzo;
Documento(String titolo, double prezzo) { this.titolo = titolo; this.prezzo = prezzo; }
public String nome() { return titolo; }
public int numeroDocumenti() { return 1; }
public double valore() { return prezzo; }
public void stampa(String rientro) { System.out.println(rientro + "- " + titolo + " (" + prezzo + ")"); }
}
/** Composite: contiene altre Voce (documenti o altre sezioni) e calcola ricorsivamente. */
class Sezione implements Voce {
private final String nome;
private final List<Voce> figli = new ArrayList<>();
Sezione(String nome) { this.nome = nome; }
Sezione aggiungi(Voce v) { figli.add(v); return this; }
public String nome() { return nome; }
public int numeroDocumenti() {
int n = 0;
for (Voce v : figli) n += v.numeroDocumenti();
return n;
}
public double valore() {
double s = 0;
for (Voce v : figli) s += v.valore();
return s;
}
public void stampa(String rientro) {
System.out.println(rientro + "+ " + nome + " [" + numeroDocumenti() + " documenti, valore " + valore() + "]");
for (Voce v : figli) v.stampa(rientro + " ");
}
}
public class CompositeEx {
public static void main(String[] args) {
Sezione narrativa = new Sezione("Narrativa")
.aggiungi(new Documento("Il nome della rosa", 12.0))
.aggiungi(new Documento("Il barone rampante", 9.5));
Sezione scienze = new Sezione("Scienze")
.aggiungi(new Documento("Fisica 1", 40.0))
.aggiungi(new Sezione("Matematica")
.aggiungi(new Documento("Analisi 1", 38.0))
.aggiungi(new Documento("Algebra lineare", 30.0)));
Sezione biblioteca = new Sezione("Biblioteca").aggiungi(narrativa).aggiungi(scienze);
biblioteca.stampa(""); // il client non distingue foglie e sezioni
System.out.println("totale: " + biblioteca.numeroDocumenti() + " documenti");
}
}+ Biblioteca [5 documenti, valore 129.5]
+ Narrativa [2 documenti, valore 21.5]
- Il nome della rosa (12.0)
- Il barone rampante (9.5)
+ Scienze [3 documenti, valore 108.0]
- Fisica 1 (40.0)
+ Matematica [2 documenti, valore 68.0]
- Analisi 1 (38.0)
- Algebra lineare (30.0)
totale: 5 documentiCompromesso di progetto: se aggiungi sta nell'interfaccia comune si ha uniformità ma le foglie devono rifiutarlo; se sta solo in Sezione si ha sicurezza di tipo ma il client deve distinguere. La struttura si scorre spesso con un Iterator (Pattern comportamentali - Observer, Strategy, Command, Iterator e Template MethodI pattern comportamentali descrivono come gli oggetti collaborano e si dividono le responsabilità: Observer (publish-subscribe) avvisa automaticamente i dipendenti quando il soggetto cambia stato, mantenendo basso l'accoppiamento (si registrano e cancellano a runtime, modello push o pull, attenzione alle modifiche durante la notifica); Strategy rende intercambiabili gli algoritmi dietro un'interfaccia (in Java anche con una lambda); Command trasforma la richiesta in un oggetto, quindi memorizzabile, accodabile e annullabile (undo); Iterator scorre un aggregato senza esporne la struttura (for-each, fail-fast); Template Method fissa lo scheletro di un algoritmo e lascia alle sottoclassi passi astratti o hook (principio di Hollywood); State, Visitor, Chain of Responsibility, Mediator, Memento e Interpreter completano le undici forme del catalogo.Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method →).
Facade
Intento. Fornire un'interfaccia unificata e semplice a un insieme di interfacce di un sottosistema; definisce un livello più alto che rende il sottosistema più facile da usare.
Quando. Un sottosistema complesso con molte classi, e la maggior parte dei client ha bisogno di poche operazioni di alto livello; si vuole ridurre l'accoppiamento tra client e sottosistema; si vogliono livelli (la Facade è il punto d'ingresso di ogni livello, vedi Architettura del software - decisioni, organizzazione e stiliL'architettura del software descrive come un sistema è organizzato in componenti, le loro relazioni e la loro distribuzione; è il ponte tra requisiti e progetto dettagliato, e le sue decisioni derivano soprattutto dai requisiti non funzionali (prestazioni, sicurezza, disponibilità, manutenibilità), spesso in conflitto tra loro; si descrive con più viste (logica, processi, sviluppo, fisica); gli stili di organizzazione ricorrenti sono a livelli (layered), MVC, repository, client-server, pipe and filter, publish-subscribe e microservizi; la modularizzazione mira ad alta coesione e basso accoppiamento, nascondendo le decisioni dietro interfacce (information hiding, inversione delle dipendenze).Architettura del software - decisioni, organizzazione e stili →). Nel GRASP è anche il Controller "di sistema" (Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →).
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Facade | Sportello |
operazione di alto livello prendiInPrestito, coordina il sottosistema |
| classi del sottosistema | Utenti, Catalogo, Prestiti, Notifiche |
fanno il lavoro, non conoscono la Facade |
| Client | main |
chiama la Facade |
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** Sottosistema: quattro classi con le loro interfacce. */
class Utenti {
private final Set<String> iscritti = new HashSet<>(Set.of("anna", "luca"));
boolean esiste(String u) { return iscritti.contains(u); }
}
class Catalogo {
private final Map<String, Integer> copie = new HashMap<>(Map.of("rosa", 1, "pendolo", 0));
boolean disponibile(String titolo) { return copie.getOrDefault(titolo, 0) > 0; }
void prelevaCopia(String titolo) { copie.merge(titolo, -1, Integer::sum); }
}
class Prestiti {
private final Map<String, String> inPrestito = new HashMap<>(); // titolo -> utente
void registra(String utente, String titolo) { inPrestito.put(titolo, utente); }
int conta() { return inPrestito.size(); }
}
class Notifiche {
void invia(String utente, String testo) { System.out.println(" [mail a " + utente + "] " + testo); }
}
/** Facade: un'unica operazione di alto livello che coordina il sottosistema. */
class Sportello {
private final Utenti utenti = new Utenti();
private final Catalogo catalogo = new Catalogo();
private final Prestiti prestiti = new Prestiti();
private final Notifiche notifiche = new Notifiche();
/** Il client chiama solo questo metodo, senza conoscere i quattro oggetti. */
boolean prendiInPrestito(String utente, String titolo) {
if (!utenti.esiste(utente)) { System.out.println("utente sconosciuto: " + utente); return false; }
if (!catalogo.disponibile(titolo)) {
notifiche.invia(utente, "'" + titolo + "' non e' disponibile");
return false;
}
catalogo.prelevaCopia(titolo);
prestiti.registra(utente, titolo);
notifiche.invia(utente, "prestito di '" + titolo + "' registrato");
return true;
}
int prestitiAttivi() { return prestiti.conta(); }
}
public class FacadeEx {
public static void main(String[] args) {
Sportello s = new Sportello();
System.out.println(s.prendiInPrestito("anna", "rosa"));
System.out.println(s.prendiInPrestito("luca", "rosa")); // nessuna copia rimasta
System.out.println(s.prendiInPrestito("piero", "pendolo")); // utente sconosciuto
System.out.println("prestiti attivi: " + s.prestitiAttivi());
}
} [mail a anna] prestito di 'rosa' registrato
true
[mail a luca] 'rosa' non e' disponibile
false
utente sconosciuto: piero
false
prestiti attivi: 1La Facade non nasconde il sottosistema (chi vuole può ancora usarlo direttamente) e non aggiunge funzioni: riunisce chiamate. Non cerca il polimorfismo: a differenza dell'Adapter non c'è un'interfaccia imposta.
Proxy
Intento. Fornire un sostituto (surrogate) di un altro oggetto per controllarne l'accesso. Ha la stessa interfaccia dell'oggetto reale, quindi il client non se ne accorge.
| ruolo | classe nel programma | responsabilità |
|---|---|---|
| Subject | Catalogo |
interfaccia comune |
| RealSubject | CatalogoRemoto |
l'oggetto vero |
| Proxy | CatalogoCache, CatalogoProtetto, il proxy dinamico |
stessa interfaccia; tiene un riferimento al reale e controlla l'accesso |
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
/** Subject: interfaccia comune a oggetto reale e proxy. */
interface Catalogo {
String descrivi(String titolo);
void elimina(String titolo);
}
/** RealSubject: il catalogo "remoto", lento da contattare. */
class CatalogoRemoto implements Catalogo {
static int chiamate = 0;
public String descrivi(String titolo) {
chiamate++;
return "scheda completa di '" + titolo + "'";
}
public void elimina(String titolo) { System.out.println(" eliminato '" + titolo + "' dal server"); }
}
/** Proxy virtuale + di cache: crea l'oggetto reale solo se serve e non ripete le richieste uguali. */
class CatalogoCache implements Catalogo {
private CatalogoRemoto reale; // creato alla prima richiesta (lazy)
private final Map<String, String> cache = new HashMap<>();
public String descrivi(String titolo) {
return cache.computeIfAbsent(titolo, t -> {
if (reale == null) reale = new CatalogoRemoto();
return reale.descrivi(t);
});
}
public void elimina(String titolo) {
if (reale == null) reale = new CatalogoRemoto();
cache.remove(titolo);
reale.elimina(titolo);
}
}
/** Proxy di protezione: lascia passare le operazioni solo a chi ha il ruolo giusto. */
class CatalogoProtetto implements Catalogo {
private final Catalogo reale;
private final String ruolo;
CatalogoProtetto(Catalogo reale, String ruolo) { this.reale = reale; this.ruolo = ruolo; }
public String descrivi(String titolo) { return reale.descrivi(titolo); }
public void elimina(String titolo) {
if (!ruolo.equals("bibliotecario")) throw new SecurityException("ruolo '" + ruolo + "' non autorizzato");
reale.elimina(titolo);
}
}
public class ProxyEx {
public static void main(String[] args) {
Catalogo c = new CatalogoCache();
System.out.println(c.descrivi("rosa"));
System.out.println(c.descrivi("rosa")); // risposta dalla cache
System.out.println("chiamate al server: " + CatalogoRemoto.chiamate);
Catalogo p = new CatalogoProtetto(c, "studente");
try { p.elimina("rosa"); } catch (SecurityException e) { System.out.println("negato: " + e.getMessage()); }
new CatalogoProtetto(c, "bibliotecario").elimina("rosa");
// Proxy dinamico del JDK (usa la riflessione): un solo handler per qualsiasi interfaccia
InvocationHandler registro = (proxy, metodo, argomenti) -> {
System.out.println(" [log] chiamato " + metodo.getName() + "(" + argomenti[0] + ")");
return metodo.invoke(c, argomenti);
};
Catalogo logged = (Catalogo) Proxy.newProxyInstance(
Catalogo.class.getClassLoader(), new Class<?>[]{Catalogo.class}, registro);
System.out.println(logged.descrivi("pendolo"));
}
}scheda completa di 'rosa'
scheda completa di 'rosa'
chiamate al server: 1
negato: ruolo 'studente' non autorizzato
eliminato 'rosa' dal server
[log] chiamato descrivi(pendolo)
scheda completa di 'pendolo'Proxy, Decorator e Adapter hanno tutti un oggetto che ne avvolge un altro, ma hanno intenti diversi: il Proxy controlla l'accesso (stessa interfaccia, spesso crea o localizza l'oggetto reale); il Decorator aggiunge funzioni (stessa interfaccia, il client sceglie quali); l'Adapter cambia interfaccia.
Bridge e Flyweight
- Bridge. Separa un'astrazione dalla sua implementazione perché possano variare indipendentemente (esempio:
Notificacon sottoclassiUrgente,Normale, che usano un'interfacciaCanaleimplementata daMail,Sms: invece di 4 classiUrgenteMail... si hanno 2+2 e la combinazione si fa per composizione). Somiglia all'Adapter nella struttura, ma l'Adapter si applica dopo (a classi già esistenti), il Bridge si progetta prima. - Flyweight. Condivide efficientemente molti oggetti a grana fine separando lo stato intrinseco (condiviso, immutabile) da quello estrinseco (passato dal client): per esempio un solo oggetto-glifo per ogni carattere in un editor, o un solo oggetto "tipo di documento" condiviso da migliaia di copie.
Integer.valueOfcon la sua cache di -128..127 è un esempio.
Domande d'esame
1. Differenza tra Adapter di classe e di oggetto: come si realizzano in Java e quando si preferisce l'uno o l'altro? Traccia. Di oggetto: l'Adapter implementa il Target e contiene un Adaptee (composizione); serve anche le sottoclassi dell'Adaptee e non ne espone i metodi. Di classe: l'Adapter estende l'Adaptee e implementa l'interfaccia Target (Java ha una sola classe base, quindi il Target deve essere un'interfaccia); è più diretto ma legato a una classe concreta ed espone i metodi dell'Adaptee. Si preferisce quello di oggetto (composizione al posto dell'ereditarietà).
2. Quale pattern usare per aggiungere assicurazione, consegna a domicilio e sconto a un prestito in tutte le combinazioni? Perché non le sottoclassi?
Traccia. Decorator: ogni opzione è un decoratore con la stessa interfaccia Prestito che contiene un Prestito; si combinano a runtime in qualsiasi ordine (e l'ordine conta per lo sconto). Con le sottoclassi servirebbero classi, e non si potrebbe cambiare a runtime.
3. Confrontare Facade e Adapter, e Proxy e Decorator. Traccia. Facade: semplifica un sottosistema senza interfaccia imposta, niente polimorfismo; Adapter: converte verso un'interfaccia esistente, spesso con polimorfismo. Proxy: stessa interfaccia, controlla l'accesso (creazione ritardata, permessi, cache); Decorator: stessa interfaccia, aggiunge responsabilità, decise dal client e combinabili.
Versione ripasso
- Strutturali: compongono classi e oggetti in strutture più grandi, disaccoppiando interfaccia e implementazione. Sette: Adapter, Decorator, Composite, Facade, Proxy, Bridge, Flyweight.
| pattern | intento | interfaccia | ruoli |
|---|---|---|---|
| Adapter | rende compatibile una classe esistente | nuova (quella del Target) | Target, Adaptee, Adapter, Client |
| Decorator | aggiunge responsabilità a runtime | stessa | Component, ConcreteComponent, Decorator (ha un Component), ConcreteDecorator |
| Composite | foglie e composti trattati allo stesso modo (alberi) | stessa | Component, Leaf, Composite (contiene Component) |
| Facade | interfaccia semplice a un sottosistema | nuova, più semplice | Facade, classi del sottosistema |
| Proxy | controlla l'accesso a un oggetto | stessa | Subject, RealSubject, Proxy |
- Adapter (wrapper): usare una classe esistente o di terzi che non si può modificare, mantenendo il polimorfismo del Target. Di oggetto: composizione, serve anche le sottoclassi dell'Adaptee, lo nasconde. Di classe:
extends Adaptee implements Target; legato a una classe concreta, espone i metodi dell'Adaptee; in Java il Target dev'essere un'interfaccia. Adattare può voler dire solo rinominare, o tutto un altro insieme di operazioni; un adapter bidirezionale supporta Target e Adaptee.
class AdapterOggetto implements RicercaCatalogo { // oggetto: composizione
private final CatalogoLegacy adaptee;
AdapterOggetto(CatalogoLegacy a) { this.adaptee = a; }
public List<String> cercaTitoli(String q) { /* adaptee.trova(q) -> lista */ }
}
class AdapterClasse extends CatalogoLegacy implements RicercaCatalogo { // classe: ereditarietà
public List<String> cercaTitoli(String q) { /* trova(q) -> lista */ }
}- Adapter contro Facade: Facade = nessuna interfaccia imposta, nessun polimorfismo, interfaccia più semplice; Adapter = interfaccia imposta, polimorfismo probabile, non semplifica.
- Errori del corso:
SortListAdapterè uno scheletro che non compila (lista non riempita,sortedNumbersnon dichiarato);MainClassAdapternon compila (len()/getFromHead()non sono diMyStack, cast sul risultato sbagliato). - Decorator: sostituisce le sottoclassi (esplosione di classi); l'ordine conta (consegna 2,00 e sconto 50%: sconto prima 2,50, dopo 1,50). Esempio JDK:
BufferedReader(InputStreamReader(System.in)). Svantaggi: tanti piccoli oggetti, identità diversa.
abstract class PrestitoDecorato implements Prestito {
protected final Prestito interno;
PrestitoDecorato(Prestito interno) { this.interno = interno; }
public double costo() { return interno.costo(); } // inoltra
}
class ConConsegna extends PrestitoDecorato {
@Override public double costo() { return interno.costo() + 2.00; }
}Uscite: base 1,00; con assicurazione 1,50; assicurazione e consegna 3,50; consegna dopo sconto 2,50; sconto dopo consegna 1,50.
- Composite:
Voce(Component) conDocumento(Leaf) eSezione(Composite) che somma ricorsivamente: biblioteca = 5 documenti, valore 129,5. Compromesso:aggiunginell'interfaccia comune (uniformità) o solo nel Composite (sicurezza).
interface Voce { String nome(); int numeroDocumenti(); double valore(); void stampa(String rientro); }
class Sezione implements Voce {
private final List<Voce> figli = new ArrayList<>();
Sezione aggiungi(Voce v) { figli.add(v); return this; }
public int numeroDocumenti() {
int n = 0;
for (Voce v : figli) n += v.numeroDocumenti(); // delega ricorsiva ai figli
return n;
}
// valore() e stampa() hanno la stessa forma ricorsiva
}- Facade:
Sportello.prendiInPrestitocoordinaUtenti,Catalogo,Prestiti,Notifiche; non nasconde il sottosistema; è il Controller di sistema di GRASP (Principi GRASP per l'assegnazione delle responsabilitàGRASP (General Responsibility Assignment Software Patterns) sono principi per assegnare le responsabilità alle classi, dove una responsabilità è un obbligo di conoscere (knowing) o di fare (doing): Creator (chi crea A? la classe che contiene o registra A, la usa da vicino o ha i dati per inizializzarla), Information Expert (la classe che ha le informazioni necessarie), Low Coupling (poche dipendenze tra classi per ridurre l'impatto dei cambiamenti), Controller (chi riceve gli eventi di sistema? un oggetto che rappresenta il sistema o il caso d'uso, che coordina e non calcola), High Cohesion (classi focalizzate su un unico scopo); si completano con Polymorphism, Pure Fabrication, Indirection e Protected Variations, che sono alla base di molti pattern GoF.Principi GRASP per l'assegnazione delle responsabilità →); punto d'ingresso di un livello.
boolean prendiInPrestito(String utente, String titolo) { // Facade: una sola operazione di alto livello
if (!utenti.esiste(utente)) return false;
if (!catalogo.disponibile(titolo)) { notifiche.invia(utente, "non disponibile"); return false; }
catalogo.prelevaCopia(titolo);
prestiti.registra(utente, titolo);
notifiche.invia(utente, "prestito registrato");
return true;
} Uscite: anna/rosa true; luca/rosa (nessuna copia) false; piero/pendolo utente sconosciuto: piero, false; prestiti attivi 1.
- Proxy: virtuale (crea l'oggetto costoso solo quando serve), di cache, di protezione (permessi), remoto, dinamico (
java.lang.reflect.Proxy,InvocationHandler). Proxy = controlla l'accesso; Decorator = aggiunge funzioni; Adapter = cambia interfaccia.
Catalogo logged = (Catalogo) Proxy.newProxyInstance(
Catalogo.class.getClassLoader(), new Class<?>[]{Catalogo.class},
(proxy, metodo, argomenti) -> metodo.invoke(c, argomenti)); // un handler per ogni interfaccia| tipo di proxy | controlla | nel programma |
|---|---|---|
| virtuale | creazione ritardata dell'oggetto costoso | CatalogoCache crea CatalogoRemoto alla prima richiesta |
| di cache | richieste ripetute | descrivi("rosa") due volte: 1 chiamata al server |
| di protezione | permessi | studente non può elimina (SecurityException), bibliotecario sì |
| dinamico | un handler per qualunque interfaccia | [log] chiamato descrivi(pendolo) |
- Adapter nel programma:
cercaTitoli("rosa")restituisce[Il nome della rosa, La rosa purpurea del Cairo]con entrambi gli adapter e[]perxyz. - Scelta tra strutturali: interfaccia incompatibile Adapter; aggiungere funzioni a runtime Decorator; gerarchia parte-tutto Composite; semplificare un sottosistema Facade; controllare o ritardare l'accesso Proxy; due dimensioni di variazione Bridge; moltissimi oggetti simili Flyweight.
- Bridge: astrazione e implementazione variano indipendentemente (2 astrazioni + 2 canali, non 4 classi); si progetta prima (l'Adapter si applica dopo). Flyweight: stato intrinseco condiviso e immutabile, estrinseco passato dal client (cache di
Integer.valueOf). - Errori tipici: adapter di classe con Target classe astratta (in Java una sola base); decorare senza pensare all'ordine; Composite con
aggiungiche le foglie ignorano in silenzio; Facade che nasconde tutto e diventa un "dio"; confondere Proxy e Decorator (intento diverso).