Home > GOF Pattern, java > GoF Patterns: Bridge

GoF Patterns: Bridge

3 febbraio 2011

Translate in English with Google Translate
In questo articolo tratterò il pattern Bridge anche detto Handle/Body.

Motivazione

Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per disaccoppiare dei componeti software. In questo modo è possibile effettuare uno switch a Run-Time, garantire il disaccoppiamento, nascondere l’implementazione, estendere la specializzazione delle classi.


Per esempio: si vuole cambiare l’interfaccia grafica della nostra applicazione da Motif a XWindow preservando la funzionalità di tutti i componenti grafici: in poche parole si vuole cambiare il LookAndFeel di tutti i tasti ma fare in modo che continuino a fare sempre la stessa cosa.
La prima idea, ma errata, sarebbe quella di creare 2 classi per ogni tasto, esempio ButtonXWindow e ButtonMotif; RadioXWindow e RadioMotif e via dicendo.  In questo modo ci sarebbe un proliferare di classi da gestire. Ovviamente se viene introdotto un nuovo LookAndFeel occorrerà inserire tutte le classi di gestione dei tasti nuovi.
In UML sarebbe così rappresentabile:

Idea Errata

Idea Errata

Occorre separare la funzionalità e l’estetica, come?

Un’altra idea, più corretta, è quella di creare 2 gerachie: una per la funzionalità ed una per l’estetica.
La funzionalità è composta dal tasto: Button, Radio.  L’estetica è data dal LookAndFell: XWindow, Motif.
Per disaccoppiare le gerachie definiamo 2 interfacce: la funzionalità nell’interfaccia Tasti e l’estetica nell’interfaccia LookAndFeel, successivamente possiamo implementare le 2 gerachie creando le classi concrete.

In UML sarebbe così rappresentabile:

Idea Corretta

Idea Corretta

Partecipanti e Struttura

Questo pattern è composto dai seguenti partecipanti:

  1. Client: colui che effettua l’invocazione all’operazione di interesse
  2. Abstraction: definisce l’interfaccia del dominio applicativo utilizzata dal Client
  3. RefinedAbstraction: definisce l’implementazione dell’interfaccia utilizzata
  4. Implementor: definisce l’interfaccia da usare come Bridge e riferibile agli oggetti concreti da utilizzare
  5. ConcreteImplementor: implementa l’interfaccia Implementor usata come Bridge per il transito degli oggetti

Possiamo schematizzare in UML

Bridge Pattern

Bridge Pattern

Conseguenze

Tale pattern presenta i seguenti vantaggi/svantaggi:

  1. disaccoppia l’interfaccia dall’implementazione: disaccoppiando Abstraction e Implementor è possibile gestire i cambiamenti delle classi concrete senza cablare nel codice dei riferiementi diretti
  2. migliora l’estendibilità: è possibile estendere la gerarchia di Abstraction e Implementor senza problemi
  3. nasconde l’implementazione al client: il Client non si deve porre il problema di conoscere l’implementazione delle classi concrete.

Implementazione

Facciamo un altro esempio: pensiamo al caso in cui ci rechiamo in un ristorante-pizzeria e facciamo un’ordinazione. Il cameriere addetto alla pizzeria prenderà la nostra ordinazione indipendentemente dal tipo di pizza che scegliamo.

Rappresentiamo questa situazione in questo Class Diagram UML:

Bridge Pattern Cameriere

Bridge Pattern Cameriere

L’interfaccia Cameriere definisce il metodo ordinazione che prende come parametro il Pasto: nel nostro caso sceglieremo una pizza.

package patterns.bridge;
public interface Cameriere {
    Pasto ordinazione(Pasto pasto);
}

La classe CamerierePizzeria implementa l’interfaccia Cameriere e ritorna
il tipo di pasto che abbiamo scelto.

package patterns.bridge;
public class CamerierePizzeria implements Cameriere {
    public Pasto ordinazione(Pasto pasto) {
        return pasto;
    }
}

L’interfaccia Pasto definisce il tipo di piatto, pertanto qualunque pietanza ipotizzabile in un ristorante-pizzeria:

package patterns.bridge;
public interface Pasto {
    Pasto getPiatto();
}

La classe PizzaCapricciosa implementa come viene fatta la pizza Capricciosa.

package patterns.bridge;
public class PizzaCapricciosa implements Pasto {
    public Pasto getPiatto() {
        return this;
    }
}

Mentre la classe PizzaMargherita implementa come viene fatta la pizza Margherita.

package patterns.bridge;
public class PizzaMargherita implements Pasto {
    public Pasto getPiatto() {
        return this;
    }
}

Siamo arrivati alla classe Cliente che effettua l’ordinazione. Il nostro cliente ordina una pizza Margherita al cameriere addetto alle pizze.

package patterns.bridge;
public class Cliente {
    public static void main(String[] args) {
        Cameriere cameriere = new CamerierePizzeria();
        Pasto ordinazione = cameriere.ordinazione(new PizzaMargherita());
        System.out.println(ordinazione);
    }
}

Eseguendo la classe Cliente abbiamo questo output:

$JAVA_HOME/bin/java patterns.bridge.Cliente
patterns.bridge.PizzaMargherita@6205b1

L’ordine della pizza Margherita è stato eseguito.
Possiamo aggiungere qualunque tipo di pizza implementando l’interfaccia Pasto disaccoppiandola con la classe CamerierePizzeria. Nascondiamo l’implementazione della pizza al cameriere che non è tenuto a sapere come viene fatta.

Estensione

Visto e considerato che ho parlato di un ristorante-pizzeria, immagino di avere un angolo ristorante servito da un cameriere diverso da quello della pizzeria. Ovviamente abbiamo anche un menù ristorante che presenta altri piatti oltre alle pizze.
Il cameriere addetto al ristorante implementa il metodo ordinazione dall’interfaccia Cameriere e si chiamerà CameriereRistorante. I pasti del ristorante implementano l’interfaccia Past.

Rappresentiamo questa situazione in questo Class Diagram UML:

Bridge Pattern Estensione

Bridge Pattern Estensione

Vediamo come si presenta la classe CameriereRistorante che si occupa di servire i clienti del ristorante.

package patterns.bridge;
public class CameriereRistorante implements Cameriere {
    public Pasto ordinazione(Pasto pasto) {
        return pasto;
    }
}

Queste invece sono le classi che si occupano di implementare l’interfaccia Pasto per gestire altre pietanze: PastaFagioli e PastaPomodoro .

package patterns.bridge;
public class PastaFagioli implements Pasto {
    public Pasto getPiatto() {
        return this;
    }
}
package patterns.bridge;
public class PastaPomodoro implements Pasto {
    public Pasto getPiatto() {
        return this;
    }
}

Eseguiamo la classe Cliente che effettua 2 ordinazioni: PizzaMargherita ed un piatto di PastaPomodoro.

package patterns.bridge;
public class Cliente {
    public static void main(String[] args) {
        Cameriere [] cameriere  = new Cameriere[2];

        cameriere[0] = new CamerierePizzeria();
        Pasto pasto = cameriere[0].ordinazione(new PizzaMargherita());
        System.out.println(pasto);

        cameriere[1] = new CameriereRistorante();
        pasto = cameriere[1].ordinazione(new PastaPomodoro());
        System.out.println(pasto);
    }
}

L’output è il seguente:

$JAVA_HOME/bin/java patterns.bridge.Cliente
patterns.bridge.PizzaMargherita@9304b1
patterns.bridge.PastaPomodoro@de6ced
Categorie:GOF Pattern, java
  1. Finl
    21 gennaio 2012 alle 10:51 AM

    Trovo l’esempio un po’ troppo semplificato e poco adatto a mostrare i vantaggi del pattern Bridge

    • 21 gennaio 2012 alle 12:56 PM

      penso che ti riferisci all’esempio del ristorante, in quanto l’esempio dell’interfaccia grafica è quello indicato nel libro GoF che non ho voluto implementare in quanto lo si può trovare facilmente.
      In generale trovare esempi che non si rivelino troppo generici o troppo specifici è l’aspetto più difficile.
      Di solito cerco di fare esempi semplici ed astratti ma il rischio è che possano essere poco pratici anche se rispettano il pattern.
      Per questo pattern, per rimanare in tema, è possibile fare altri esempi più specifici.
      Infatti considerando che è anche detto “Driver” oltre ad “Handle/Body”, un esempio è proprio quello dei driver delle schede video, stampanti ecc.
      Invece in ambito java un esempio lo troviamo nell’architettura JDBC che realizza l’astrazione mentre l’implementazione ed il supporto ai diversi database avviene con la realizzazione di librerie specifiche che definiscono i dettagli implementativi.

      Puoi trovare degli esempi qui:
      sourcemaking, jugtorino, programcreek, oodesign, prasanthaboutjava

  2. Anonimo
    29 giugno 2012 alle 9:27 AM

    Sto studiando il pattern bridge anche se non ho ben capito quali sono i vantaggi …
    cioè non è la stessa cosa di quando usiamo classi astratte che saranno implementate da classi concrete?

    • 29 giugno 2012 alle 7:31 PM

      Pensa di avere 2 interfacce (o classi astratte) diverse, che si riferiscono a 2 famiglie di prodotti distinti pertanto non sono parenti.
      Tra di loro esiste un legame ma di tipo associativo in cui una interfaccia ha una reference all’altra interfaccia.
      Quindi invece di esserci un legame IS-A (parentela) c’è un legame HAS-A (associazione).
      A questo punto ogni famiglia avrà le proprie implementazioni e queste sono indirettamente associate tra di loro.
      Perchè fare tutto ciò?
      Nel primo esempio, quello dei tasti e della loro visualizzazione, vengono create 2 famiglie e poi vengono collegate tra di loro per scegliere in base al tasto il tipo di visualizzazione.
      Il collegamento avviene a run-time, pertanto non esiste un vincolo “cablato” nel codice tra le 2 famiglie.
      In JDBC, in cui viene utilizzato questo pattern, lo Statement (interfaccia) implementato da CallableStatement, PreparedStatement è collegato al Driver (interfaccia) che a sua volta è implemetato da diversi vendor (Oracle, MySql ecc) quindi le diverse istruzioni usano diversi drivers.

  3. Alessio
    19 novembre 2012 alle 11:08 PM

    Ciao, sto facendo una relazione sul bridge e la tua spiegazione è stata veramente utile, dettagliata e semplice da capire, grazie… Avevo già un’idea sulla struttura di questo pattern, ma mi hai chiarito molti dubbi.

  4. 9 marzo 2013 alle 8:51 PM

    Ciao, ho letto la spiegazione: mi pare di avere capito che l’accesso ai diversi piatti avvenga utilizzando un cameriere pizzeria per la pizza e un cameriere ristorante per il piatto di pasta. Credo che la cosa dipenda da come funziona il pattern, quindi che possa esserci un’implementazione in cui un unico cameriere possa servire entrambi i piatti. È vero?

    • 17 marzo 2013 alle 10:58 AM

      Si, se il tuo obiettivo è quello di avere un cameriere con ordinazioni variegate pizza/pasta è possibile, usando una sola astrazione Cameriere ed n implementazioni. Il cameriere prende le ordinazioni ai tavoli e li porta in cucina per la preparazione, quindi una interfaccia (cameriere) e molte implementazioni (pasta/pizze/torte/ecc.). Il cameriere avrà una lista aggregata di piatti da far implementare ma per ogni elemento della lista deve essere scelta solo una implementazione tra quelle possibili.
      L’obiettivo del pattern e’ quello di disaccoppiare l’astrazione con l’implementazione. Nel passato si spingeva soprattutto verso l’ereditarietà pertanto si sarebbero create delle classi che implementavano l’astrazione. Adesso, da “Effective Java”, si spinge verso la composizione in modo da non creare legami forti (http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance), infatti tra l’Abstraction e l’Implementor c’è una relazione di aggregazione quindi un HAS-A, preferita rispetto ad un IS-A.

  5. Anonimo
    6 settembre 2013 alle 4:42 PM

    Ciao, io non capisco nel caso della pizzeria/ristorante quali sono Implementor e ConcreteImplementor.

    • 6 settembre 2013 alle 5:38 PM

      L’Implementor è rappresentato dalla interfaccia Pasto mentre i ConcreteImplementor sono rappresentati da PizzaMargherita, PizzaCapricciosa, PastaFagioli e PastaPomodoro. Questi rappresentano la reale implementazione delle logiche.
      Mentre invece Abstraction (Cameriere) e RefinedAbstraction (CamerierePizzeria e CameriereRistorante ) rappresentano le “interfacce” del dominio applicativo, la prima “Cameriere” è una interfaccia java mentre le seconde “CamerierePizzeria e CameriereRistorante” sono le classi che la implementano.

  6. Andrea
    6 agosto 2014 alle 10:43 AM

    Ciao mi puoi spiegare meglio che intendi quando nei vantaggi scrivi disaccoppia l’interfaccia dall’implementazione?

    • 6 agosto 2014 alle 1:11 PM

      Nel bridge si evidenziano 2 strutture, la prima espone l’interfaccia (Abstraction) mentre la seconda espone l’implementazione (Implementor).
      In questo contesto, con questi termini, non ci si riferisce alla sintassi java di interface e class mentre invece ci si riferisce ad un concetto più astratto.
      L’interfaccia(Abstraction) indica la modalità in cui le informazioni vengono esposte verso il chiamante cioè le funzioni esposte, mentre l’implementazione (Implementor) indica la modalità di implementazione di queste funzioni.
      In java le due strutture sono realizzate utilizzando sintatticamente interface (I) e class (C).
      Quindi la prima struttura che espone l’interfaccia(Abstraction) è composta da: Cameriere (I) e CamerierePizzeria(C) mentre la seconda struttura che espone l’implementazione (Implementor) è composta da: Pasto (I), PizzaMargherita(C) e PizzaCapricciosa(C).
      Disaccoppiare l’interfaccia(Abstraction) dall’implementazione(Implementor) significa che il client può scegliere a run-time a fronte di una funzionalità esposta dall’interfaccia (della prima struttura ) quale classe (della seconda struttura) utilizzare per l’implementazione.
      Nel primo esempio il disaccoppiamento avviene quando il cameriere (Abstraction) invoca l’implementazione (Implementor).
      In questo caso l’implementazione non è legata all’interfaccia tramite ereditarietà ma utilizza il concetto di delega, ossia un intermediario effettua il legame:

      cameriere.ordinazione(new PizzaMargherita());
      

      Per cercare di spiegarmi nel migliore dei modi ho pensato di fare un esempio per confrontare l’utilizzo di una struttura ad oggetti classica e l’utilizzo di una struttura ad oggetti utilizzando il pattern bridge.

      Immaginiamo una interfaccia che deve esporre la funzione di stampa, e l’implementazione deve prevedere di stampare “Ciao” oppure “Hello”.

      Secondo un approccio ad oggetti classico allora creo una interfaccia e 2 classi concrete che la implementano:
      Definisco l’interfaccia che espone il metodo:

      package interfaccia.accoppiamento;
      
      public interface InterfacciaStampa {
          void stampa();
      }
      

      Definisco la prima implementazione del metodo che stampa “Ciao”:

      package interfaccia.accoppiamento;
      
      public class StampaCiao implements InterfacciaStampa{
      
          @Override
          public void stampa() {
              System.out.println("Ciao");
          }
      
      }
      

      Definisco la seconda implementazione del metodo che stampa “Hello”:

      package interfaccia.accoppiamento;
      
      public class StampaHello implements InterfacciaStampa{
      
          @Override
          public void stampa() {
              System.out.println("Hello");
          }
      
      }
      

      Creo il Client che effettua la chiamata:

      package interfaccia.accoppiamento;
      
      public class Client {
      
          public static void main(String[] args) {
              //Esiste un legame forte tra abstraction (InterfacciaStampa) ed implementor (StampaCiao e StampaHello)
              InterfacciaStampa interfacciaStampa = new StampaCiao();
              interfacciaStampa.stampa();
      
              interfacciaStampa = new StampaHello();
              interfacciaStampa.stampa();
          }
      
      }
      

      Secondo un approccio ad oggetti utilizzando il pattern bridge allora creo le strutture di interfacce e le rispettive classi concrete che le implementano.
      Prima struttura che definisce l’astrazione
      Interfaccia che espone il metodo:

      package interfaccia.disaccoppiamento.abstraction;
      
      import interfaccia.disaccoppiamento.implementor.ImplemetazioneStampa;
      
      public interface InterfacciaStampa {
          void stampa(ImplemetazioneStampa implemetazioneStampa);
      }
      

      Implementazione dell’astrazione che realizza il bridge, ossia il ponte di collegamento tra la strutture di astrazione e quella di implementazione:

      package interfaccia.disaccoppiamento.abstraction;
      
      import interfaccia.disaccoppiamento.implementor.ImplemetazioneStampa;
      
      public class EsecutoreInterfacciaStampa implements InterfacciaStampa{
      
          @Override
          public void stampa(ImplemetazioneStampa implemetazioneStampa) {
              implemetazioneStampa.stampaImpl();
          }
      
      }
      

      Seconda struttura che definisce l’implementazione:

      package interfaccia.disaccoppiamento.implementor;
      
      public interface ImplemetazioneStampa {
          void stampaImpl();
      }
      
      package interfaccia.disaccoppiamento.implementor;
      
      public class StampaCiao implements ImplemetazioneStampa{
      
          @Override
          public void stampaImpl() {
              System.out.println("Ciao");
          }
      
      }
      
      package interfaccia.disaccoppiamento.implementor;
      
      public class StampaHello implements ImplemetazioneStampa{
      
          @Override
          public void stampaImpl() {
              System.out.println("Hello");
          }
      
      }
      

      Creo il Client che effettua la chiamata:

      package interfaccia.disaccoppiamento;
      
      import interfaccia.disaccoppiamento.abstraction.EsecutoreInterfacciaStampa;
      import interfaccia.disaccoppiamento.abstraction.InterfacciaStampa;
      import interfaccia.disaccoppiamento.implementor.StampaCiao;
      import interfaccia.disaccoppiamento.implementor.StampaHello;
      
      public class Client {
      
          public static void main(String[] args) {
              //L'abstraction (InterfacciaStampa) è disaccoppiata dall'implementor (StampaCiao e StampaHello)
              //in quanto viene attivato la delega: EsecutoreInterfacciaStampa
              InterfacciaStampa interfacciaStampa = new EsecutoreInterfacciaStampa();
              interfacciaStampa.stampa(new StampaCiao());
          
              interfacciaStampa = new EsecutoreInterfacciaStampa();
              interfacciaStampa.stampa(new StampaHello());
          
          }
      
      }
      

      In entrambi i casi l’output è lo stesso:

      Ciao
      Hello
      

      Il vantaggio ricade nel fatto che le due strutture sono completamente separate, non esiste un legame forte (da codice), quindi possono essere pacchettizzate in jar differenti ed essere suscettibili di versionamento, ossia mentre l’interfaccia resta la stessa, è possibile linkare a run-time la versione della libreria di implementazione che preferiamo.
      Ti riporto il commento indicato nel libro GoF a proposito della domanda che mi hai fatto e un link di approfondimento bridge
      In ogni caso spero di essere stato chiaro. Ciao.

      Dal libro “Design Patterns Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides Addison-Wesley; 1st edition (January 15, 1995)”

      Decoupling interface and implementation. An implementation is not bound permanently to an interface. The implementation of an abstraction can be configured at run-time. It’s even possible for an object to change its implementation at run-time.
      Decoupling Abstraction and Implementor also eliminates compile-time dependencies on the implementation. Changing an implementation class doesn’t require recompiling the Abstraction class and its clients. This property is essential when you must ensure binary compatibility between different versions of a class library.
      Furthermore, this decoupling encourages layering that can lead to a better-structured system. The high-level part of a system only has to know about Abstraction and Implementor.

  1. No trackbacks yet.
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: