Home > GOF Pattern, java > Simple, Double e Multi Dispatch in Java

Simple, Double e Multi Dispatch in Java

28 novembre 2012

Il polimorfismo
La programmazione ad oggetti si basa su alcuni concetti basilari quali ereditarietà, incapsulamento e polimorfismo. Il polimorfismo è uno dei concetti più complessi, soprattutto per chi proviene da un linguaggio di tipo procedurale o di tipo strutturato, in quanto utilizza un grado di astrazione nello sviluppo del software. Il polimorfismo rende possibile l’utilizzo di una serie di pattern, favorisce il riuso del software, il disaccoppiamento dei componenti e principalmente la determinazione a runtime dei comportamenti superando quei limiti imposti dai linguaggi monomorfici come il PASCAL.

Spesso il suo utilizzo è associato al polimorfismo universale ad inclusione anche detto polimorfismo ereditario in cui la sua implementazione è legata al concetto di ereditarietà la quale permette di creare un grado comune di parentela, di ereditare, di sovrascrivere e di aggiungere dei comportamenti. Il polimorfismo è legato al concetto di binding dinamico, reso possibile nei linguaggi che consentono di linkare a runtime gli oggetti da invocare.

Il polimorfismo viene classificato in questo modo:

  • Polimorfismo
    • Universale
      • Parametrico: quando viene definito il tipo di oggetto in base ad un parametro, come per i Generic
      • Inclusione: quando viene definita una classe comune nella gerarchia di ereditarietà conforme al principio di sostituibilità di Liskov
    • ad hoc
      • Overloading: quando vengono creati dei metodi con lo stesso nome ma parametri di ingresso diversi (per tipo e/o numero)
      • Coercisione: quando viene effettuato un down-casting esplicito o un up-casting implicito

Come esempio consideriamo un caso d’uso frequente, il polimorfismo universale per inclusione (anche detto polimorfismo ereditario o polimorfismo di sottotipo).

  1. Definiamo un albero di ereditarietà padre/figlio composto da 2 classi: VisitatorePadre e VisitatoreFiglio.
  2. Definamo una terza classe (Elemento) con un metodo accetta() con un tipo comune in ingresso: VisitatorePadre.
  3. Definiamo una quarta classe (Client)che effetti l’invocazione.

Vediamo prima come si presenta il Class Diagram in UML:

la sua implementazione sarà la seguente:

Creiamo la classe Padre

package dispatch.single;
public class VisitatorePadre {}

la classe Figlia:

package dispatch.single;
public class VisitatoreFiglio extends VisitatorePadre {}

la classe che detiene il metodo polimorfico:

package dispatch.single;

public class Elemento {

    void accetta(VisitatorePadre visitatore) {
        System.out.print( "1) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
        System.out.println( " con parametro[" + visitatore.getClass().getName() +"]");
    }

}

la classe Client che si occupa di effettuare le invocazioni

package dispatch.single;

public class Client {
    public static void main(String[] args) {
        Elemento elemento = new Elemento();

        System.out.println( "Primo caso" );
        elemento.accetta( new VisitatorePadre() );

        System.out.println( "Secondo caso" );
        elemento.accetta( new VisitatoreFiglio() );
    }
}

L’output dell’esecuzione e’ il seguente:

$JAVA_HOME/bin/java dispatch.single.Client
Primo caso
1) Invocazione metodo[void dispatch.single.Elemento.accetta(dispatch.single.VisitatorePadre)] con parametro[dispatch.single.VisitatorePadre]

Secondo caso
1) Invocazione metodo[void dispatch.single.Elemento.accetta(dispatch.single.VisitatorePadre)] con parametro[dispatch.single.VisitatoreFiglio]

L’oggetto passato come parametro nel metodo polimorfico potrà quindi essere uno dei tipi padre/figlio.

Single Dispatch
La convenienza del polimorfismo si concretizza utilizzando l’overriding dei metodi che consente di determinare un comportamento diverso a fronte di oggetti diversi appartenenti allo stesso tipo.
Verifichiamo aggiungendo un metodo ad entrambre le classi padre/figlio, modifichiamo il Class Diagram ed il codice java.

Vediamo prima come si presenta il Class Diagram in UML:

l’implementazione diventerà la seguente:

Modifico la classe Padre per definire il metodo da invocare:

package dispatch.single;

public class VisitatorePadre {

    void visita() {
           System.out.println( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]\n");
    }

}

Modifico la classe Figlia per effettuare l’overwrite del metodo:

package dispatch.single;

public class VisitatoreFiglio extends VisitatorePadre {

    @Override
    void visita() {
       System.out.println( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]\n");
    }
}

Modifico la classe Elemento per invocare il metodo in base all’oggetto instanziato:

package dispatch.single;

public class Elemento {

    void accetta(VisitatorePadre visitatore) {
        System.out.print( "1) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
        System.out.println( " con parametro[" + visitatore.getClass().getName() +"]");
        visitatore.visita();
    }

}

Eseguo il Client:

$JAVA_HOME/bin/java dispatch.single.Client
Primo caso
1) Invocazione metodo[void dispatch.single.Elemento.accetta(dispatch.single.VisitatorePadre)] con parametro[dispatch.single.VisitatorePadre]
2) Invocazione metodo[void dispatch.single.VisitatorePadre.visita()]

Secondo caso
1) Invocazione metodo[void dispatch.single.Elemento.accetta(dispatch.single.VisitatorePadre)] con parametro[dispatch.single.VisitatoreFiglio]
2) Invocazione metodo[void dispatch.single.VisitatoreFiglio.visita()]

Vediamo come si presenza l’invocazione in base al Sequenze Diagram:

Siamo in presenza di un caso di “Single Dispatch” cioè l’azione da invocare ( Dispatch ) dipende dall’oggetto ( Single ) passato come parametro. In base all’oggetto passato come parametro, verrà invocato il relativo metodo.
Single Dispatch ? Eh si, l’uso del polimorfismo combinato con l’overriding!
Il “Single Dispatch” è utilizzato in parecchi pattern GoF (Factory Method, Bridge ecc) e con la Dependency Injection ha trovato un terreno fertile di utilizzo e si basa sui concetti di ereditarietà, polimorfismo ed overriding.

Double Dispatch
Se l’azione da intraprendere dipende da più oggetti passati come parametro, siamo in presenza di un Double o Multiple Dispatch.
Java supporta nativamente il “Single Dispatch” mentre non supporta nativamente il “Double Dispatch” o il “Multi Dispatch” pertanto occorre utilizzare dei pattern, quali il “Visitor Pattern”.
Mentre nel caso del Single Dispatch utilizziamo il polimorfismo ereditario e l’overriding, nel “Double Dispatch” utilizziamo il polimorfismo ereditario e overloading.
Per verificarlo modifichiamo le classi in modo da permettere il passaggio del parametro aggiuntivo.

Rivediamo il Class Diagram UML:

Double Dispatch

Double Dispatch

Definiamo la classe Padre che si occupa di invocare il metodo del secondo oggetto:

package dispatch.doubles;

public class VisitatorePadre {

    void visita(ElementoPadre elemento) {
       System.out.print( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
       System.out.println( " con parametro[" + elemento.getClass().getName() +"]");
       elemento.esegui();
    }

    void visita(ElementoFiglio elemento) {
       System.out.print( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
       System.out.println( " con parametro[" + elemento.getClass().getName() +"]");
       elemento.esegui();
    }

}

Facciamo lo stesso per la classe Figlia:

package dispatch.doubles;

public class VisitatoreFiglio extends VisitatorePadre {

    @Override
    void visita(ElementoPadre elemento) {
       System.out.print( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
       System.out.println( " con parametro[" + elemento.getClass().getName() +"]");
       elemento.esegui();
    }

    @Override
    void visita(ElementoFiglio elemento) {
       System.out.print( "2) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
       System.out.println( " con parametro[" + elemento.getClass().getName() +"]");
       elemento.esegui();
    }
}

Definiamo nel metodo dell’ElementoPadre l’invocazione del metodo visita() questa volta passando come parametro “se stesso”, che rappresenterà il secondo oggetto:

package dispatch.doubles;

public class ElementoPadre {

    void accetta(VisitatorePadre visitatore) {
       System.out.print( "1) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod().toGenericString() +"]");
       System.out.println( " con parametro [" + visitatore.getClass().getName() +"]");
       visitatore.visita(this);
    }

    void esegui(){
       System.out.println( "3) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod().toGenericString() + "]\n");
    }

}

Seguiamo lo stesso approccio anche per la classe ElementoFiglio :

package dispatch.doubles;

public class ElementoFiglio extends ElementoPadre {

    @Override
    void accetta(VisitatorePadre visitatore) {
       System.out.print( "1) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() +"]");
       System.out.println( " con parametro[" + visitatore.getClass().getName() +"]");
       visitatore.visita(this);
    }

    @Override
    void esegui(){
       System.out.println( "3) Invocazione metodo[" + new Object(){}.getClass().getEnclosingMethod() + "]\n");
    }
}

Definamo nella classe Client gli step di invocazione:

package dispatch.doubles;

public class Client {
    public static void main(String[] args) {
        ElementoPadre elemento = null;

        elemento = new ElementoPadre();

        System.out.println( "Primo caso" );
        elemento.accetta( new VisitatorePadre() );

        System.out.println( "Secondo caso" );
        elemento.accetta( new VisitatoreFiglio() );

        elemento = new ElementoFiglio();

        System.out.println( "Terzo caso" );
        elemento.accetta( new VisitatorePadre() );

        System.out.println( "Quarto caso" );
        elemento.accetta( new VisitatoreFiglio() );
    }
}

Eseguiamo il tutto:

$JAVA_HOME/bin/java dispatch.doubles.Client
Primo caso
1) Invocazione metodo[void dispatch.doubles.ElementoPadre.accetta(dispatch.doubles.VisitatorePadre)] con parametro [dispatch.doubles.VisitatorePadre]
2) Invocazione metodo[void dispatch.doubles.VisitatorePadre.visita(dispatch.doubles.ElementoPadre)] con parametro[dispatch.doubles.ElementoPadre]
3) Invocazione metodo[void dispatch.doubles.ElementoPadre.esegui()]

Secondo caso
1) Invocazione metodo[void dispatch.doubles.ElementoPadre.accetta(dispatch.doubles.VisitatorePadre)] con parametro [dispatch.doubles.VisitatoreFiglio]
2) Invocazione metodo[void dispatch.doubles.VisitatoreFiglio.visita(dispatch.doubles.ElementoPadre)] con parametro[dispatch.doubles.ElementoPadre]
3) Invocazione metodo[void dispatch.doubles.ElementoPadre.esegui()]

Terzo caso
1) Invocazione metodo[void dispatch.doubles.ElementoFiglio.accetta(dispatch.doubles.VisitatorePadre)] con parametro[dispatch.doubles.VisitatorePadre]
2) Invocazione metodo[void dispatch.doubles.VisitatorePadre.visita(dispatch.doubles.ElementoFiglio)] con parametro[dispatch.doubles.ElementoFiglio]
3) Invocazione metodo[void dispatch.doubles.ElementoFiglio.esegui()]

Quarto caso
1) Invocazione metodo[void dispatch.doubles.ElementoFiglio.accetta(dispatch.doubles.VisitatorePadre)] con parametro[dispatch.doubles.VisitatoreFiglio]
2) Invocazione metodo[void dispatch.doubles.VisitatoreFiglio.visita(dispatch.doubles.ElementoFiglio)] con parametro[dispatch.doubles.ElementoFiglio]
3) Invocazione metodo[void dispatch.doubles.ElementoFiglio.esegui()]

Vediamo la sequenza di invocazione nel Primo caso:

Primo caso: Double Dispatch

Primo caso: Double Dispatch

Vediamo la sequenza di invocazione nel Secondo caso:

Secondo caso: Double Dispatch

Secondo caso: Double Dispatch

Vediamo la sequenza di invocazione nel Terzo caso:

Terzo caso: Double Dispatch

Terzo caso: Double Dispatch

Vediamo la sequenza di invocazione nel Quarto caso:

Quarto caso: Double Dispatch

Quarto caso: Double Dispatch

Implementazione
Vediamo un caso pratico e semplice di implementazione del Double Dispatch.
Ho pensato di simulare il gioco della Morra Cinese (sasso, forbici, carta). In questo caso si tratta di realizzare tre oggetti di tipo Segno pertanto il “Double Dispatch” sfrutterà una sola catena di ereditarietà.

Vediamo il Class Diagramm UML

Definiamo la classe Segno che prevederà tutti i metodi necessari:

package dispatch.doubles.morraCinese;

public abstract class Segno {

    public abstract void contro(Segno segno);

    public abstract void sfida(Carta carta);
    public abstract void sfida(Sasso sasso);
    public abstract void sfida(Forbice forbice);

    public abstract void vince();
    public abstract void perde();
    public abstract void pari();

}

La classe Carta utilizza il “Single Dispatch”, difatti implementa il metodo contro(), con l’overriding e l’uso del polimorfismo ereditario usando il tipo Segno ed inoltre utilizza “Double Dispatch” difatti passa se stesso (this) al metodo sfida(), con l’overloading.

package dispatch.doubles.morraCinese;

public class Carta extends Segno {

    @Override
    public void contro(Segno segno) {
        System.out.print(this.getClass().getSimpleName() + " contro " + segno.getClass().getSimpleName() + ": ");
        segno.sfida(this);
    }

    @Override
    public void sfida(Carta carta) {
        carta.pari();
    }

    @Override
    public void sfida(Sasso sasso) {
        sasso.perde();
    }

    @Override
    public void sfida(Forbice forbice) {
        forbice.vince();
    }

    @Override
    public void vince() {
        System.out.println("Carta vince!");
    }

    @Override
    public void perde() {
        System.out.println("Carta perde!");
    }

    @Override
    public void pari() {
        System.out.println("Pari!");
    }

}

Stesso ragionamento per la classe Sasso:

package dispatch.doubles.morraCinese;

public class Sasso extends Segno {

    @Override
    public void contro(Segno segno) {
        System.out.print(this.getClass().getSimpleName() + " contro " + segno.getClass().getSimpleName() + ": ");
        segno.sfida(this);
    }

    @Override
    public void sfida(Carta carta) {
        carta.vince();
    }

    @Override
    public void sfida(Sasso sasso) {
        sasso.pari();
    }

    @Override
    public void sfida(Forbice forbice) {
        forbice.perde();
    }

    @Override
    public void vince() {
        System.out.println("Sasso vince!");
    }

    @Override
    public void perde() {
        System.out.println("Sasso perde!");
    }

    @Override
    public void pari() {
        System.out.println("Pari!");
    }

}

Stesso ragionamento per la classe Forbice:

package dispatch.doubles.morraCinese;

public class Forbice extends Segno {

    @Override
    public void contro(Segno segno) {
        System.out.print(this.getClass().getSimpleName() + " contro " + segno.getClass().getSimpleName() + ": ");
        segno.sfida(this);
    }

    @Override
    public void sfida(Carta carta) {
        carta.perde();
    }

    @Override
    public void sfida(Sasso sasso) {
        sasso.vince();
    }

    @Override
    public void sfida(Forbice forbice) {
        forbice.pari();
    }

    @Override
    public void vince() {
        System.out.println("Forbice vince!");
    }

    @Override
    public void perde() {
        System.out.println("Forbice perde!");
    }

    @Override
    public void pari() {
        System.out.println("Pari!");
    }

}

Definisco la classe Client per provare le combinazioni.

package dispatch.doubles.morraCinese;

public class Client {

    public static void main(String[] args) {
        Carta carta = new Carta();
        Sasso sasso = new Sasso();
        Forbice forbice = new Forbice();

        carta.contro(sasso);
        carta.contro(carta);
        carta.contro(forbice);

        sasso.contro(sasso);
        sasso.contro(carta);
        sasso.contro(forbice);

        forbice.contro(sasso);
        forbice.contro(carta);
        forbice.contro(forbice);
    }
}

Eseguo il Client e tutto funziona a perfezione, senza dover usare istruzioni IF!🙂

$JAVA_HOME/bin/java dispatch.doubles.morraCinese.Client
Carta contro Sasso: Carta vince!
Carta contro Carta: Pari!
Carta contro Forbice: Carta perde!
Sasso contro Sasso: Pari!
Sasso contro Carta: Sasso perde!
Sasso contro Forbice: Sasso vince!
Forbice contro Sasso: Forbice perde!
Forbice contro Carta: Forbice vince!
Forbice contro Forbice: Pari!

Vediamo come si presenta il primo Sequence Diagram in base all’invocazione carta vs sasso:

Link utili di approfondimento:
On Understanding Types, Data Abstraction, and Polymorphism

Multiple Dispatch in Practice

Categorie:GOF Pattern, java
  1. 24 aprile 2014 alle 12:12 PM

    credo che nell’ultimo Sequenze Diagram vi sia una imprecisione:

    sfida(Sasso) al posto di
    sfida(Carta)

    o viceversa…invertire i due case…

    Sasso.sfida(Carta)

    • 25 aprile 2014 alle 7:47 PM

      No, non mi sembra.
      La chiamata che viene effettuata passa per questi oggetti: Client->Carta->Sasso->Carta.
      Ho semplificato e commentanto il codice, in modo da poter controllare con maggiore semplicità.
      Per il metodo contro() si effettua l’overwrite in prima invocazione mentre per il metodo sfida() si effettua l’overloading in seconda invocazione.
      La classe Carta invoca il metodo sfida della classe Sasso passando “se stesso” (this) come parametro, quindi Sasso.sfida(Carta).
      Di seguito la classe Sasso, nel metodo sfida(Carta), invoca il metodo vince della classe Carta, quindi Carta.vince().
      Spero di essere stato più chiaro.

      Classe Client

      public class Client {
       
          public static void main(String[] args) {
              Carta carta = new Carta();
              Sasso sasso = new Sasso();
              //1.Invocazione del metodo "contro" in overwrite
              carta.contro(sasso);
          }
      }
      

      Classe Carta

      public class Carta extends Segno {
       
          @Override
          public void contro(Segno segno) {
              System.out.print(this.getClass().getSimpleName() + " contro " + segno.getClass().getSimpleName() + ": ");
              //2.Invocazione del metodo "sfida" in overloading passando "se stesso" come parametro: oggetto "this"
              segno.sfida(this);
          }
         
          @Override
          public void vince() {
              //4.Esecuzione del metodo
              System.out.println("Carta vince!");
          }
      }
      

      Classe Sasso

      public class Sasso extends Segno {
      
          @Override
          public void sfida(Carta carta) {
              //3.utilizzo dell'oggetto passato per l'invocazione doppia: double dispatch
              carta.vince();
          }
      }
      
  2. 25 aprile 2014 alle 4:23 PM

    Avevo lasciato un commento, ma sembra che non sia stato considerato; anzi è stato addirittura cancellato. Se ho scritto qualcosa che non andava, avrei voluto saperne anche i motivi. Oppure dipende dal mio profilo, dove si evince che sono un maschilista e quindi da evitare come la peste? Lo so con la decadenza odierna, se mi dichiarassi femminista sarei certamente più favorito, ma sarebbe come negare me stesso.

    • 25 aprile 2014 alle 8:31 PM

      Assolutamente, il tuo commento era solo in attesa di approvazione. Tutti i commenti che ricevo vengono messi nello stato “da approvare” altrimenti lo spam invaderebbe il blog. Appena posso, rispondo. Non faccio nessuna discriminazione, di nessun tipo, a parte per lo spam!🙂

  3. andriun
    29 ottobre 2014 alle 8:20 PM

    Giuseppe Dell’Abate, mi accorgo solo ora, che non l’ho ancora ringraziata per le cortesi spiegazioni che mi ha fornito, con estrema pazienza e chiarezza. Bene, anche se certamente per lei non cambia nulla è comunque mia intenzione farlo qui una volta per tutte, Grazie!

    • 12 novembre 2014 alle 12:16 PM

      Spero di sbagliarmi ma dal tono mi sembra un ringraziamento sarcastico.
      Sicuramente non sono riuscito a rispondere a te, come ad altri, a tutte le domande che mi hai posto, e per questo mi dispiace.
      Il motivo è solo dovuto al poco tempo a disposizione che posso dedicare al mio blog, infatti è anche da un pò di tempo che non riesco a pubblicare altri articoli.
      Rispondo alle domande in modo randomico e non cronologico, senza alcuna preferenza nei confronti di chi mi ha posto la domanda.
      Questo blog nasce come taccuino di appunti condiviso pertanto non ho l’ambizione di fornire un servizio.
      Mi fa piacere se l’hai trovato interessante ed in esso hai trovato materiale utile su questi argomenti ma se cercavi maggiore interazione allora mi dispiace ma non posso garantirla a te come a tutti. Ciao.

  1. 15 giugno 2013 alle 2:18 AM
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: