Salta al contenuto
Note per Studenti Pattern strutturali - Adapter, Decorator, Composite, Facade e Proxy

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.

java
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/SortListAdapter il metodo sort(int[]) è solo uno scheletro: converte un array in lista senza riempirla, chiama l'adaptee e restituisce sortedNumbers, mai dichiarato; non compila. Va riempita la lista con i valori dell'array e ricostruito l'array dalla lista restituita. (2) MainClassAdapter degli esempi non compila: usa il tipo MyStack<String> per la variabile ma poi chiama len() e getFromHead(), 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
java
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 studenti

L'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
java
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 documenti

Compromesso 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
java
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: 1

La 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
java
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: Notifica con sottoclassi Urgente, Normale, che usano un'interfaccia Canale implementata da Mail, Sms: invece di 4 classi UrgenteMail... 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.valueOf con 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 232^3 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.
java
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, sortedNumbers non dichiarato); MainClassAdapter non compila (len()/getFromHead() non sono di MyStack, 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.
java
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) con Documento (Leaf) e Sezione (Composite) che somma ricorsivamente: biblioteca = 5 documenti, valore 129,5. Compromesso: aggiungi nell'interfaccia comune (uniformità) o solo nel Composite (sicurezza).
java
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
}
java
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 →\to true; luca/rosa (nessuna copia) →\to false; piero/pendolo →\to 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.
java
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 [] per xyz.
  • Scelta tra strutturali: interfaccia incompatibile →\to Adapter; aggiungere funzioni a runtime →\to Decorator; gerarchia parte-tutto →\to Composite; semplificare un sottosistema →\to Facade; controllare o ritardare l'accesso →\to Proxy; due dimensioni di variazione →\to Bridge; moltissimi oggetti simili →\to 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 aggiungi che le foglie ignorano in silenzio; Facade che nasconde tutto e diventa un "dio"; confondere Proxy e Decorator (intento diverso).

Esercizi su questo argomento

Teoria collegata