• Aucun résultat trouvé

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

8.2. Généricité des algorithmes

Nous abordons ici la généricité même des algorithmes. Les manières de rendre un algorithme indépendant des structures de données qu’il manipule ont été discutées à la section précédente. Nous présentons maintenant une façon de le concevoir plus indépendant des sous-algorithmes qu’il emploie. Ensuite, nous discutons de différentes approches pour le rendre paramétrable, afin qu’il soit extensible ultérieurement. Pour des raisons de clarté, les figures de cette section ne présentent pas les algorithmes sous forme de patrons comme il l’a été décidé à la section précédente, nous considérons implicitement qu’ils sont paramétrés sur les données portées par les noeuds et les arcs.

8.2.1. Abstraction des algorithmes

Il existe un patron de conception très connu, appelé stratégie (cf. [Gamm95]), qui consiste à modéliser un algorithme sous la forme d’un objet possédant une méthode, run()par exemple, appelée pour exécuter l’algorithme. Il est alors possible de proposer une classe abstraite pour représenter une catégorie d’algorithmes (e.g. les algorithmes pour résoudre le problème de la tension de coût minimale, cf. chapitre 4).

« abstraite » AlgoTensionM in M iseConformité + run(Graphe *) + défaut() : AlgoTensionMin * Méthode virtuelle.

M iseE chelleDual Agrégation

return new MiseEchelleDual;

+ setParamètres(...) + run(Graphe *) + setParamètres(...) + run(Graphe *) + setParamètres(...) + run(Graphe *) AlgoSynchronisation « utilise » + setParamètres(...) + run(a : AlgoTensionMin *) Graphe * g = ...; ... a.run(g); ...

Figure 8.7: Un exemple d’abstraction d’une catégorie d’algorithmes.

Comme le montre la figure 8.7, cette classe abstraite fournit une interface commune à tous les algorithmes, ce qui les rend totalement interchangeables lorsqu’ils sont ensuite utilisés dans un autre algorithme, e.g. la classe

AlgoSynchronisationsur la figure. La virtualité impliquée ici est négligeable dans la plupart des cas,

puisque les méthodes appelées ainsi sont des algorithmes complets, i.e. avec un nombre d’opérations nettement supérieur à celui induit par la virtualité. En outre, l’interface étant commune à tous, elle ne peut pas être utilisée pour fournir des paramètres spécifiques à un algorithme. Au patron stratégie, il faut donc ajouter pour chacun une méthode propre, e.g.setParamètres(), dont le rôle est d’initialiser les paramètres de l’algorithme.

AlgoTensionMin * a = new MiseConformité;

AlgoSynchronization * s = new AlgoSynchronisation; s.setParamètres(...);

a.setParamètres(...); s.run(a);

L’exemple précédent montre qu’il est ainsi possible de décider de l’algorithme de tension de coût minimal à employer pendant l’exécution du programme, de le créer et de le paramétrer avant de le fournir à l’algorithme

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

AlgoSynchronisation. Il faut noter que le rôle des méthodessetParamètres()peut être joué par les

con-structeurs des algorithmes. Ainsi, au moment de leur création ces derniers sont systématiquement paramétrés. Il semble également important, lorsque l’on dispose de plusieurs méthodes pour résoudre un même problème, d’en fournir une par défaut, afin d’éviter que l’utilisateur ne se perde dans des détails inutiles sur les perfor-mances des algorithmes. C’est le rôle de la méthode de classedéfaut()deAlgoTensionMin, qui fournit a priori l’algorithme reconnu le plus performant. Mais il est tout à fait possible d’envisager une approche plus évoluée oùdéfaut()reçoit le graphe à traiter en paramètre, et à partir d’une analyse (e.g. la mesure de la densité du graphe) propose l’algorithme le mieux adapté. Cette méthode de classe est également importante pour l’évolution des programmes des utilisateurs. En l’utilisant un composant bénéficie automatiquement des progrès apportés par l’intégration d’une nouvelle méthode plus performante dans la bibliothèque.

8.2.2. Extension des algorithmes

Nous proposons ici trois manières de rendre un algorithme extensible, l’idée étant que certaines parties de l’algorithme sont déléguées dans des méthodes qui peuvent être remplacées par le réutilisateur. Dans la suite de notre discussion, même si les figures ne le montrent pas toujours, nous considérons que les algorithmes suivent le patron stratégie, avec les évolutions proposées à la section précédente, à savoir une méthode pour paramétrer chaque algorithme et une méthode de classe qui fournit l’algorithme le mieux adapté.

8.2.2.1. Approche par méthode virtuelle

La première approche, le patron de conception méthode paramètre (template method [Gamm95]), consiste tout simplement, de la méthoderun()d’un algorithme, à déporter une partie du code dans des méthodes virtuelles, les méthodes paramètres, appartenant toujours à l’algorithme. Ainsi, par héritage, ces méthodes peuvent être modifiées, et sans altérer le code de la méthode run() peuvent changer son comportement. La figure 8.8 illustre ce mécanisme. + setParamètres(...) + run(...) # opération1(...) # opération2(...) Méthode virtuelle. Méthode virtuelle. ... opération1(...); ... opération2(...); ... Algorithme1 Algorithme2 « abstraite » Algorithme + setParamètres(...) # opération1(...) # opération2(...) + setParamètres(...) # opération1(...) # opération2(...)

Figure 8.8: Un exemple d’extension d’un algorithme par méthodes virtuelles.

Le défaut majeur de cette approche est évidemment l’emploi du mécanisme de virtualité qui peut, si les méth-odes paramètres sont souvent appelées, entraîner une perte de performance conséquente. Le second défaut de cette technique est la rigidité de l’extension de l’algorithme. Il est en effet impossible en temps réel de proposer un paramétrage différent de ceux prévus par les sous-classes de l’algorithme.

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

8.2.2.2. Approche par visiteur abstrait

La seconde approche repose sur le concept de visiteur, également un patron de conception proposé dans [Gamm95]. Il consiste à modéliser les méthodes paramètres comme un objet. Plus précisément, le visiteur pos-sède des méthodes qui correspondent aux méthodes paramètres. Pour qu’un objet algorithme fonctionne (i.e. sa méthoderun()soit opérationnelle) il faut qu’il agrège un objet visiteur qui lui fournit les parties manquantes de son code. Cet objet peut être fourni par exemple à la construction de l’algorithme. La figure 8.9 présente la classe Algorithmequi manipule dans sa méthode run()un objet visiteur implémentant l’interface de la classe Visiteur, cette dernière fournit les méthodes paramètres opération1() etopération2() nécessaires à l’algorithme. Algorithme + constructeur(v : Visiteur *) + setParamètres(...) + run(...) ... visiteur.opération1(...); ... visiteur.opération2(...); ... + opération1(...) + opération2(...) visiteur « abstraite » Visiteur Visiteur1 Visiteur2 visiteur = v; + setParamètres(...) + opération1(...) + opération2(...) + setParamètres(...) + opération1(...) + opération2(...)

Figure 8.9: Un exemple d’extension d’un algorithme par visiteur abstrait.

L’exemple suivant montre la flexibilité apportée par la technique dans l’extension des algorithmes. Il est en effet possible de définir à l’exécution quel sera le visiteur d’un algorithme. Il faut noter que le visiteur peut avoir ses propres paramètres et qu’ils doivent être initialisés avant l’appel à l’algorithme principal.

Visiteur1 * v = new Visiteur1;

Algorithme * a = new Algorithme(v); a.setParamètres(...);

v.setParamètres(...); a.run(...);

Cependant, le défaut majeur, que l’on retrouvait déjà dans la première approche, réside dans l’utilisation du mécanisme de virtualité pour appeler les méthodes paramètres (e.g. les méthodes opération1() et

opération2()du visiteur).

8.2.2.3. Approche par concept de visiteur

Pour éviter finalement le mécanisme de virtualité, il suffit de fournir le visiteur en paramètre et non pas en argument à l’algorithme, autrement dit ce dernier doit être un patron paramétré sur le type du visiteur qu’il manipule. Celui-ci doit alors simplement implémenter le concept de visiteur nécessaire à l’algorithme. La figure 8.10 illustre cette approche. L’exemple suivant montre que l’utilisation du visiteur est très similaire à l’approche précédente, la seule réelle différence à ce niveau étant que l’utilisateur n’a pas à créer explicitement de visiteur.

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

Algorithme<Visiteur1> * a = new Algorithme<Visiteur1>; a.getVisiteur().setParamètres(...);

a.setParamètres(...); a.run(...);

Le mécanisme de virtualité étant écarté, la technique présentée ici ne présente aucune perte d’efficacité. En revanche elle ne permet aucune flexibilité dans la construction des algorithmes, la relation algorithme-visiteur est fixée à la compilation. Cette flexibilité est pourtant importante si l’on considère qu’une méthode (comme nous l’avons expliqué dans la section sur le patron stratégie) est capable d’analyser la structure d’un graphe, de déterminer et de fournir l’algorithme le mieux adapté grâce à un paramétrage dynamique de cet algorithme avec des visiteurs.

Algorithme + run(...) + getVisiteur() : Visiteur * V « interface » Visiteur + opération1(...) + opération2(...) Algorithme<Visiteur1> visiteur visiteur Visiteur1 Visiteur2 Algorithme<Visiteur1> Algorithme<Visiteur2> ... visiteur.opération1(...); ... visiteur.opération2(...); ... V visiteur « utilise » « implémente » + setParamètres(...) + opération1(...) + opération2(...) + setParamètres(...) + opération1(...) + opération2(...)

Figure 8.10: Un exemple d’extension d’un algorithme par concept de visiteur.

8.2.2.4. Conclusion

Pour permettre une bonne généricité des algorithmes, il semble donc important d’appliquer le patron stratégie avec les modifications que nous avons proposées à la section 8.1, tout en le combinant avec l’approche par concept de visiteur pour permettre une extension efficace des algorithmes. En ce qui concerne les différentes bibliothèques proposées pour la recherche opérationnelle, il faut savoir que seule BGL propose réellement une extension des algorithmes, les autres fournissant plutôt des algorithmes dans un but unique.

L’approche employée dans cette bibliothèque est très proche, mais plus générique, que celle que nous avons retenue. Nous avons expliqué les raisons qui nous ont fait choisir une voie moins intéressante pour les réutilisa-teurs, mais plus réaliste pour les concepteurs. Il faut également noter que la bibliothèque STL emploie la notion de foncteur très similaire à la notion de concept de visiteur. D’autres approches sont proposées, notamment dans [Dure01] qui s’applique à proposer des versions génériques (i.e. avec des patrons de composant) d’un grand nombre de patrons de conception proposés dans [Gamm95].

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

8.2.3. Gestion de données additionnelles

Il est très courant pour un algorithme d’avoir besoin d’affecter des données supplémentaires aux éléments des structures de données qu’il manipule. Par exemple un algorithme de résolution d’un problème de flot peut gérer un potentiel sur les noeuds du graphe, mais l’utilisateur ne doit pas connaître ce détail et se limiter aux données de flot. Nous discutons ici brièvement plusieurs manières de gérer ces données additionnelles.

La première approche consiste à stocker les données dans une structure à part du graphe. Un vecteur, par exemple, permet de conserver ces données dans le même ordre que les noeuds dans le graphe. L’accès aux données est immédiat, mais interdit toute modification du graphe qui perturberait l’ordre des noeuds. Il est alors possible d’utiliser un conteneur associatif pour stocker les données, par exemple un arbre binaire de recherche où l’identifiant d’un noeud joue le rôle d’une clé pour rechercher les données associées au noeud dans l’arbre. Cette approche autorise une modification structurelle du graphe, mais l’accès aux données est beaucoup plus lent (O(log n) opérations pour n noeuds dans le graphe).

Nous avons donc recherché une approche qui allie un accès efficace des données additionnelles à la flexibilité du graphe. Elle consiste à prévoir dans les classes des noeuds et des arcs un attribut qui référence pour chacun une zone mémoire que l’on appellera zone de travail. Cette zone n’est absolument pas gérée (ni même allouée) par la classe du graphe, mais par les algorithmes qui vont manipuler le graphe. Ainsi, lorsqu’un algorithme a besoin d’ajouter des données à un noeud, il lui suffit d’allouer ces données et de faire pointer l’attribut de la zone de travail dessus. A partir du noeud, l’accès aux données additionnelles est ainsi immédiat. Le code suivant illustre cette approche.

Graphe<Flot,Potentiel> * g = new Graphe<Flot,Rien>; ...

g.noeud(3).zoneTravail() = new Potentiel; ...

(Potentiel *)(g.noeud(3).zoneTravail()).potentiel = 2.5;

Cet exemple montre comment ajouter une donnée de la classePotentiel(cf. section 8.1) à un noeud, mais soulève un problème important qui est qu’un downcast est nécessaire pour récupérer la donnée avec le bon type. Nous avons discuté à la section 8.1 des défauts de cette opération. La zone de travail doit néanmoins référencer n’importe quel type de données. Les langages de programmation ont différentes manières de représenter une référence sur un objet quelconque. Java propose la classe Object dont toute classe hérite directement ou indirectement. C++ propose le typevoid *qui représente une référence d’un type quelconque.

Un problème se pose également si plusieurs algorithmes ont besoin de la zone de travail, par exemple un algorithme de tension utilise la zone de travail mais fait appel à un algorithme de plus court chemin qui lui aussi a besoin de cette zone. Une approche consisterait, au début de chaque algorithme qui gère des données additionnelles, à sauvegarder les références de la zone de travail de chaque noeud par exemple avant de les remplacer par ses propres références. Il suffit alors de les restituer à la fin de la méthode, afin que l’algorithme appelant retrouve la zone de travail dans l’état où il l’a laissée.

Cela ne coûte qu’un nombre linéaire d’opérations en fonction du nombre de noeuds et reste négligeable pour la plupart des algorithmes. Cependant, il est possible d’obtenir une meilleure efficacité, en proposant simplement, au lieu d’une référence, une pile de références sur des zones de travail. Chaque algorithme qui gère des données additionnelles ajoute sa référence sur la pile, et celui qui est effectivement en cours d’exécution (on exclue ici une utilisation multithread) manipulera toujours la zone de travail au sommet de la pile.

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