Le design pattern Double dispatch

Réparer les problèmes de typage lors d'un héritage.

Le but de ce tutoriel est d'expliquer le principe du double dispatch qui est un composant essentiel du design pattern Visiteur.

Un cas concret sans le double dispatch

Vous êtes le codeur fou d'un jeu de poker qui ne connait pas le double dispatch.

Du point de vue conception logicielle vous aurez des classes Dame, Roi, As etc.. et une classe Carte dont héritent toutes les autres. Vous aurez aussi un Dealer (le distributeur de cartes) avec la capacité d'annoncer les cartes.

Les classes du jeu sans double dispatch

enum Couleur { TREFLE, COEUR, CARREAU, PIQUE }; class Carte { protected Couleur couleur; public Carte() {} public String toString() { return "Je suis une carte mais je ne te dirai pas laquelle, dommage hein ?"; } } class Roi extends Carte { public Roi(Couleur couleur) { this.couleur = couleur; } public String toString() { return " " + couleur; } } class Personne { public void annoncer(Carte carte) { System.out.println("Personne - voici la carte : " + carte); } public void annoncer(Roi roi) { System.out.println("Personne - Longue vie au roi de " + roi); } } class Dealer extends Personne { public void annoncer(Carte carte) { System.out.println("Dealer - voici la carte : " + carte); } public void annoncer(Roi roi) { System.out.println("Dealer - Longue vie au roi de " + roi); } } public class LanceurMain { public static void main(String[] args) { Carte riviere = new Roi(Couleur.TREFLE); Personne dealer = new Dealer(); dealer.annoncer(riviere); } }

Lors d'une partie pour tester le jeu avec vos amis vous arrivez à la rivière (la dernière carte à être retournée). Le suspens est insoutenable. La carte est retournée, le jeu doit l'afficher sur l'écran : "Dealer - voici la carte : TREFLE".

D'après ce qui est affiché, on en déduit que le programme a compris que c'est un dealer qui a annoncé la carte. Le dealer n'a pas deviné tout de suite qu'il s'agissait d'un roi puisqu'il ne lui a pas souhaité "longue vie", pourtant c'est bien le roi qui a répondu en annonçant sa couleur.

Que s'est-il passé ?

Le design pattern Double dispatch

Java et C++, pour la résolution dynamique des types, implémentent le simple dispatch : le type dynamique de l'objet appelant la méthode peut être déduit à l'exécution. Dans l'exemple, la variable dealer a pour type statique Personne et pour type dynamique Dealer. Le compilateur pense que dealer est de type Personne. Le vrai type, Dealer donc, est déduit à l'exécution du programme, ce qui permet d'appeler la méthode "annoncer" de la classe Dealer.

Malheureusement Java et C++ n'implémentent pas le multiple dispatch : ils ne peuvent pas déduire dynamiquement le type des arguments des méthodes. La variable "riviere" a pour type statique Carte et pour type dynamique Roi. Au moment du passage en argument, riviere est une Carte puisque son type n'est pas défini dynamiquement. En revanche, lors de l'appel à la méthode toString, riviere est bien considérée comme un Roi.

Pour palier à ce défaut nous allons simuler le multiple dispatch à l'aide du design pattern Double dispatch. Notez que je parle de défaut car ça nous embête dans le cas actuel mais beaucoup pensent qu'autoriser le multiple dispatch rendrait pas mal de code sale. Nous ne sommes pas là pour faire le débat mais si vous voulez donner votre avis dans les commentaires, ne vous privez pas.

Autant le problème était dur à comprendre, autant la solution est très simple à mettre en oeuvre : il faut inverser l'appelant et l'argument. Dans les classes Carte et Roi nous aurons une méthode "annoncePar" qui prendra comme argument une Personne. Il suffit ensuite d'appeler la méthode "annoncer" de cette personne avec "this" en argument. La différence avec la situation précédente, c'est que le type de "this" est déduit à la compilation.

Les classes du jeu avec double dispatch

class Carte { protected Couleur couleur; public Carte() {} public String toString() { return "Je suis une carte mais je ne te dirai pas laquelle, dommage hein ?"; } public void annoncePar(Personne p) { p.annoncer(this); } } class Roi extends Carte { public Roi(Couleur couleur) { this.couleur = couleur; } public String toString() { return " " + couleur; } public void annoncePar(Personne p) { p.annoncer(this); } } public class LanceurMain { public static void main(String[] args) { Carte riviere = new Roi(Couleur.TREFLE); Personne dealer = new Dealer(); riviere.annoncePar(dealer); } }

Et voici le résultat tant attendu : "Dealer - Longue vie au roi de TREFLE". Revoyons le déroulement du programme par étape :

  • Debut : riviere est une Carte, dealer une Personne.
  • Appel de la méthode riviere.annoncePar(dealer) : riviere est dynamiquement déduite comme Roi ce qui permet d'appeler la bonne méthode, dealer est passée en argument en tant que Personne.
  • Appel de la méthode p.annoncer(this); : dealer est dynamiquement déduite comme Dealer et nous savons déjà que this est un Roi. C'est donc la méthode annoncer(Roi roi) qui est choisie.

Bien sûr d'autres techniques fonctionnent, par exemple l'utilisation de "if (carte instanceof Roi)" au lieu de créer plusieurs méthodes "annoncer". Les design pattern existent justement pour éviter ce genre de code non maintenable à long terme.