next up previous contents index
Next: Paquets et accessibilité Up: Patterns Previous: Extension d'une interface

   
Une discipline d'abstraction

L'usage conjoint des classes et des interfaces permet de découpler l'implémentation de la déclaration : aux interfaces la déclaration des méthodes publiques (de leur nom, des types de leurs paramètres, de leur type de retour), aux classes l'implémentation des méthodes (leur corps, c'est-à-dire ce qu'elles font) et la construction des objets. Pour assurer ce découplage, on observera autant que possible les deux règles suivantes, qui constituent une discipline d'abstraction pour l'utilisateur, c'est-à-dire du côté du client :  

L'idée est de choisir séparément les fonctionnalités souhaitées, ce qui détermine l'interface (par exemple, a-t-on besoin d'une méthode de comparaison ?), et la représentation des données et l'implémentation des méthodes, ce qui détermine les classes d'implémentation.

Ce découplage est particulièrement utile quand plusieurs implémentations existent ; nous en verrons des exemples à propos de la famille des collections. Voici l'exemple des piles, dont le type abstrait est formé des déclarations suivantes :

interface Pile {
  boolean estVide();
  void empiler(Object o);
  Object sommet();
  Object dépiler();
}

Notons qu'il s'agit d'un type générique au sens où n'importe quel objet (c'est-à-dire instance de la classe Object) peut être empilé. Un programme utilisant des piles doit connaître ce type abstrait, et peut ignorer la nature de l'implémentation des piles (par un tableau, une liste chaînée, etc.). Il doit même ignorer cette implémentation, afin d'être indépendant de l'implémentation choisie, laquelle doit être modifiable sans remettre en cause les modules qui l'utilisent. Il ne faut donc pas qu'un programme utilisant des piles comporte des expressions du style p.tableau[p.hauteur - 1], qui n'ont de sens que pour une implémentation particulière des piles. Il suffit au programme de connaître le nom d'une classe d'implémentation. Cela ne signifie pas que le choix d'une classe d'implémentation est arbitraire ; des considérations d'efficacité, mais aussi de disponibilité des classes d'implémentation guident généralement ce choix. Cependant, ce découplage met en \oeuvre la liaison tardive, qui est moins efficace qu'une liaison déterminée à la compilation.

Voici par exemple une méthode toString()  que l'on pourrait ajouter à la classe ArbreBinaire, afin d'imprimer les étiquettes d'un arbre parcouru en profondeur d'abord ; on utilise la première implémentation, pour laquelle l'arbre vide est représenté par la valeur null :

class ArbreBinaire {
  int étiquette;
  ArbreBinaire gauche;
  ArbreBinaire droit;
  // ...
  public String toString() {
    StringBuffer tampon = new StringBuffer();
    Pile p = new PileParListe();
    p.empiler(this);
    while (!p.estVide()) {
      ArbreBinaire t = (ArbreBinaire) p.depiler();
      if (t != null) {
        tampon.append(t.étiquette).append(" ");
        p.empiler(t.droit);
        p.empiler(t.gauche);
      }
    }
    return tampon.toString();
  }
}

Ici, PileParListe est une implémentation de l'interface Pile. Il est inutile d'en connaître le contenu pour définir la méthode imprimer(). Si l'on préférait utiliser une autre implémentation de Pile, soit PileParTableau, il suffirait de remplacer l'appel au constructeur PileParListe() par un appel au constructeur PileParTableau(). Du fait de la généricité de Pile, la valeur retournée par dépiler() est de type Object ; avant de l'affecter à t, on lui applique un transtypage vers le type ArbreBinaire, de façon à pouvoir accéder aux membres de la classe ArbreBinaire à travers t.


next up previous contents index
Next: Paquets et accessibilité Up: Patterns Previous: Extension d'une interface
R. Lalement
2000-10-23