• Aucun résultat trouvé

Chapitre 8 - Une Expérience de Réutilisabilité pour les Problèmes de Graphes 159

8.1.3. Abstraction par les itérateurs

Dans beaucoup de traitements, les structures de données qui sont manipulées sont des collections d’objets (liste, tableau, pile, file d’attente, arbre...). En particulier le graphe possède deux collections, l’une d’arcs et l’autre de noeuds. Nous discutons ici d’une manière d’abstraire la structure même de ces collections, afin que les traitements sur celles-ci soient indépendants de la structure de données effectivement manipulée. Les approches présentées ici peuvent être étendues à l’abstraction de tout type de structure de données.

+ suivant() + fini() : booléen + getElément() : T « interface » Itérateur T Itérateur2 + suivant() + fini() : booléen + getElément() : T T Collection1 T Itérateur1 + suivant() + fini() : booléen + getElément() : T T Collection2 T Algorithme T « utilise » « ami »

« ami » + getItérateur() : Itérateur2<T>

...

« implémente »

+ getItérateur() : Itérateur1<T> ...

Figure 8.4: Un exemple d’itérateur.

Dans notre discussion, nous considérerons l’exemple d’une procédure qui parcours tous les éléments d’une collection et leur applique un traitement, et nous nous intéresserons à rendre son code unique pour tout type de collection. Le principe consiste à intercaler, pour chaque collection, une classe entre la collection et les algorithmes qui la manipulent. Cette classe possède la même interface quelque soit la collection. En l’utilisant

CHAPITRE8 - UNEEXPÉRIENCE DERÉUTILISABILITÉ POUR LESPROBLÈMES DEGRAPHES Bruno Bachelet

de préférence aux méthodes propres à la collection, les algorithmes deviennent plus indépendants de la structure qu’ils manipulent et les collections plus facilement interchangeables. L’interface qui convient à notre exemple est un itérateur, il s’agit d’un patron de conception proposé dans [Gamm95] et exploité intensivement dans la bibliothèque STL.

L’interface d’un itérateur propose la fonctionnalité de déplacement dans une collection triée (implicitement ou explicitement). La figure 8.4 montre le principe d’un itérateur qui offre les méthodes pour se déplacer dans un sens arbitraire d’un élément à un autre d’une collection. Dans notre exemple, nous disposons de deux collections qui peuvent créer un ou plusieurs itérateurs (grâce à leur méthode getItérateur()) sur leur propre structure.

Il est à noter qu’une collection doit accéder à des attributs normalement cachés pour initialiser un itérateur et inversement un itérateur doit accéder à la structure cachée de sa collection. Les deux classes doivent donc être mutuellement amies, cela signifie que chacune autorise explicitement l’autre à accéder à ses données cachées (tous les langages n’implémentent pas cette fonctionnalité, en C++ il existe le mot-cléfriendqu’une classe peut utiliser pour en autoriser une autre à accéder à ses données cachées, en Java cette fonctionnalité n’est pas directement implémentée, mais par le mécanisme de classe interne, il est possible que certaines classes voient des choses normalement cachées pour d’autres). Nous présentons maintenant différentes manières d’utiliser les itérateurs qui traduisent différents niveaux d’abstraction d’une collection pour un algorithme.

8.1.3.1. Abstraction par les itérateurs

Le premier niveau d’abstraction consiste, même si l’on connaît précisément une structure, par exemple un objet de la classeCollection1, à utiliser ses itérateurs pour le manipuler, en évitant les méthodes propres à la structure. Comme le montre l’exemple suivant, il est ainsi possible, même dans un code a priori non réutilisable, de modifier la structure de données sans modifier aucune autre ligne de code.

Collection1<T> c = ...;

Itérateur1<T> i = g.getItérateur();

while not i.fini() do ...i.getElément()... ...

i.suivant(); end while;

Si l’on remplace les classesCollection1et Iterateur1par les classesCollection2etItérateur2, aucune autre ligne n’a besoin d’être modifiée pour que le code fonctionne encore. Cette première abstraction d’une structure de données est intéressante mais insuffisante puisqu’elle nécessite une intervention manuelle, néanmoins il est toujours conseillé d’employer cette approche qui ne coûte absolument rien en efficacité, ni même en temps de développement et qui, à défaut d’une meilleure approche, apporte une certaine maintenabilité au code.

8.1.3.2. Abstraction par paramétrage des itérateurs

Le second niveau d’abstraction consiste à paramétrer un algorithme sur les classes des itérateurs qu’il manipule. En reprenant l’exemple précédent, il est possible de proposer un algorithme paramétré sur le type de l’itérateur utilisé, le paramètre Idans l’exemple qui suit. L’algorithme n’aura aucune connaissance de la structure qu’il manipule et n’utilisera que les itérateurs qui lui sont fournis en argument.

Bruno Bachelet CHAPITRE8 - UNEEXPÉRIENCE DERÉUTILISABILITÉ POUR LESPROBLÈMES DEGRAPHES

function algo<I>(I * i) while not i.fini() do

...i.getElément()... ...

i.suivant(); end while; end function;

L’algorithme nécessite alors simplement un paramètre qui implémente le concept d’itérateur pour fonctionner. L’inconvénient de cette approche est que l’utilisateur de l’algorithme connaît des détails du fonctionnement interne de la fonction (dans notre cas, que l’algorithme parcours un à un les éléments), ce qui peut rompre le concept d’encapsulation. En outre, si l’algorithme a besoin de beaucoup d’itérateurs, l’utilisateur doit les fournir, sans forcément en comprendre les raisons. Cette approche a été celle choisie par la STL, mais il faut reconnaître que les inconvénients que nous venons de citer ne s’y appliquent pas, puisque le transfert d’itérateurs aux algorithmes est justifié.

8.1.3.3. Abstraction par paramétrage de la structure de données

Aux vues des inconvénients des approches précédentes, un troisième niveau d’abstraction a été proposé notam-ment dans [Lee99], qui a suivi les premiers travaux de [Kuhl96] (utilisant plutôt le second niveau d’abstraction), et a débuté la conception de la bibliothèque BGL.

+ Itérateur

« interface »

Collection T

+ getItérateur() : Itérateur Collection2

+ Itérateur + getItérateur() : Itérateur T Collection1 + Itérateur + getItérateur() : Itérateur « implémente » T Alias de la classe Itérateur1. Alias de la classe Itérateur2. Itérateur2 + suivant() + fini() : booléen + getElément() : T T Itérateur1 + suivant() + fini() : booléen + getElément() : T T « ami » « ami » + suivant() + fini() : booléen + getElément() : T « interface » Itérateur T « implémente » Algorithme T « utilise » « utilise »

Figure 8.5: Un exemple d’abstraction d’une collection.

Au lieu de paramétrer l’algorithme sur les itérateurs, pourquoi ne pas le paramétrer sur la structure de don-nées même qui doit être manipulée. La structure doit alors implémenter un concept qui permet la création d’itérateurs pour la parcourir. La figure 8.5 montre l’interface que doit posséder une collection. Mais il faut tout de même que l’algorithme connaisse le type de l’itérateur qu’il manipule. Comme on ne souhaite pas fournir explicitement d’itérateur en paramètre à l’algorithme, il ne peut y avoir que la collection qui possède ce renseignement. Pour cela, il est possible de définir des types internes à une classe qui sont accessibles comme toute autre propriété de la classe.

CHAPITRE8 - UNEEXPÉRIENCE DERÉUTILISABILITÉ POUR LESPROBLÈMES DEGRAPHES Bruno Bachelet

En reprenant notre exemple, le type interneItérateur(représenté en italique sur la figure) est défini dans l’interfaceCollection, il s’agit en fait d’un alias sur la véritable classe de l’itérateur de la collection. Dans la classeCollection1, il s’agit donc du typeItérateur1. L’exemple suivant illustre cette manière d’abstraire la structure de données. Cette approche fournit une abstraction complète, les collections sont simplement obligées d’implémenter un certain concept relatif au mécanisme des itérateurs.

function algo<C>(C * c)

c.Itérateur i = c.getItérateur(); while not i.fini() do

...i.getElément()... ...

i.suivant(); end while; end function;

Le seul véritable défaut de cette approche (également présent dans le deuxième niveau d’abstraction) se situe au niveau pratique. Il est en effet très difficile de déboguer un patron d’algorithme élaboré avec ce type de paramètre. La raison en est très simple. Lorsque le patron est analysé à la compilation, avant toute instancia-tion, le typeC, si l’on revient à l’exemple précédent, est inconnu. Il est donc impossible de vérifier que le type

C.Iterateur(ou le typeIpour l’exemple du deuxième niveau d’abstraction) existe et surtout implémente

le concept d’itérateur. Il faut donc attendre une instanciation du patron pour démarrer une véritable vérifica-tion du code. Ceci est très problématique lorsque l’on possède de nombreux modules à compiler et que le patron se trouve tôt dans la compilation alors que son instanciation s’effectue très tard. La phase de débogage, par la longueur des tentatives de compilation, devient rapidement presque impossible. En revanche, en tant qu’utilisateur, cette dernière approche présente la meilleure alternative, puisqu’elle offre le plus d’indépendance entre un algorithme et les structures de données qu’il manipule.

8.1.3.4. Un compromis d’abstraction

Comme précisé précédemment, la bibliothèque BGL propose cette dernière approche qui satisfait pleinement les utilisateurs, mais rend la tâche de la conception de la bibliothèque difficile. LEDA ne semble pas poursuivre tout à fait le même objectif de rendre les structures de données indépendantes des algorithmes. Sa manière de concevoir les composants se rapproche de notre choix, puisque nous proposons une abstraction intermé-diaire qui tente de satisfaire à la fois les concepteurs (qui sont aussi réutilisateurs) et les utilisateurs finaux. Considérons l’exemple suivant qui illustre notre approche.

function algo<TA,TN>(Graphe<TA,TN> * g) g.ItérateurArc i = g.getItérateurArc(); while not i.fini() do

...i.getElément()... ...

i.suivant(); end while; end function;

Il est possible de paramétrer les algorithmes sur les types des données portées par les noeuds et les arcs, et dans l’algorithme d’appliquer le tout premier niveau d’abstraction, c’est-à-dire manipuler directement la classe

Graphepour récupérer, à la manière du troisième niveau d’abstraction, les classes des itérateurs. L’algorithme

Bruno Bachelet CHAPITRE8 - UNEEXPÉRIENCE DERÉUTILISABILITÉ POUR LESPROBLÈMES DEGRAPHES

n’est alors pas vraiment indépendant de la structure de graphe, puisqu’il manipulera toujours la classeGraphe. Cependant, toutes les manipulations se font par les itérateurs. Ainsi, un utilisateur qui désire changer de struc-ture de graphe peut le faire sans avoir à modifier autre chose. Mais cela a un impact sur toute la bibliothèque, puisqu’il ne peut exister qu’une seule classeGraphe.

+ ItérateurArc + ItérateurNoeud « interface » G rap he TA,TN + getItérateurArc() : ItérateurArc + getItérateurNoeud() : ItérateurNoeud « interface » Arc + getOrigine() : Noeud<TA,TN> + getDestination() : Noeud<TA,TN> « interface » Noeud + getItérateurArcsEntrants(): ItérateurArc + getItérateurArcsSortants(): ItérateurArc + ItérateurArc G rap he Noeud TN donnée Arc TA donnée arcsEntrants arcsSortants destination origine * * * * + suivant() + fini() : booléen + getElément() : T « interface » Itérateur T Algorithme « utilise » « utilise » « implémente » « implémente » « implémente » « utilise » « utilise » TA,TN TA,TN TA,TN TA,TN TA,TN TA,TN

Figure 8.6: L’abstraction d’un graphe.

Notre bibliothèque est ainsi nettement moins flexible, mais lorsque nous nous penchons sur la manière de réutiliser les algorithmes, on s’aperçoit qu’il est très délicat de manipuler plusieurs structures de graphe. Imag-inons que l’on soit en train d’élaborer une nouvelle méthode, basée sur des algorithmes existants. Si l’un des algorithmes fonctionne efficacement avec une structure de graphe et un autre avec une structure totale-ment différente, quelle structure choisir pour notre méthode ? Celle de l’un des deux algorithmes existants ou une structure qui sera un compromis pour les deux algorithmes ? L’utilisateur se trouve alors confronté à un dilemme pour lequel il ne possède pas tous les détails; et d’ailleurs dans un souci d’encapsulation, il ne doit pas. Il connaît tout au plus quelle structure est la mieux adaptée à un algorithme.

Ainsi, pour l’utilisation que nous envisageons de notre bibliothèque, un développement rapide de prototypes et une vitesse raisonnable des algorithmes pour un produit fini, nous avons choisi de fixer la structure de données du graphe. Nous détaillons légèrement en annexe notre choix. La figure 8.6 résume finalement le modèle que nous avons retenu. Certes il n’est pas le meilleur mais sans une évolution notable des compilateurs C++ sur la vérification des patrons, il nous semble peu envisageable de maintenir une bibliothèque comme la nôtre avec une abstraction totale de la structure d’un graphe.

CHAPITRE8 - UNEEXPÉRIENCE DERÉUTILISABILITÉ POUR LESPROBLÈMES DEGRAPHES Bruno Bachelet