• Aucun résultat trouvé

Structure du graphe des dépendances modulaires

Chapitre 5 EVOLUTION DU PROJET EM2

5.3. l Implantation du noyau

8.2.3 Structure du graphe des dépendances modulaires

La modularité, au sens des langages ADA ou Modula-2, pennet de décomposer un logiciel en un certain nombre de modules. Il existe des méthodes facilitant la décomposition d'un projet en modules [Par 72) [Wie 84), mais la part laissée à l'intuition du concepteur est encore assez importante pour que la décomposition modulaire d'un projet soit prôpre à ce concepteur. En règle générale, si plusieurs concepteurs développent un même projet, il est quasiment certain que leurs décompositions modulaires seront différentes. Il faut aussi remarquer que la qualité de la structure d'un logiciel, aussi bonne soit elle initialement, a tendance à se dégrader au cours de la vie de ce logiciel.

Topologie du graphe des dépendances modulaires

Le problème de la structure modulaire d'un programme a été analysé depuis longtemps d'une façon très générale. Ains~ Wirth [Wir 74]

a décrit cette structure en termes de niveaux d'abstraction, où les niveaux inférieurs correspondent à un raffinement croissant des niveaux supérieurs. D'une manière similaire, Dahl, Dijkstra et Hoare [Dah 72] ont défini chaque niveau comme une ressource abstraite ou une machine virtuelle, fournissant des outils de base au niveau supérieur, et réalisé à l'aide du niveau inférieur.

Depuis, on est arrivé à l'hypothèse suivante: un programme bien structuré donne lieu à un graphe de dépendances hiérarchique et arborescent [Tur 80). Le critère de hiérarchie implique que les modules soient répartis sur des niveaux distincts, et celui d'arborescence implique qu'il n'y ait pas de partage d'un module entre plusieurs modules de la couche supérieure. Nous avons déjà aperçu un certain nombre de critères de modularisation au paragraphe 5.2.5 du chapitre précédent. On s'est rendu compte que ces deux critères ne sont pas entièrement compatibles avec ceux de types abstraits ou de réutilisation.

Nous pensons qu'il faut trouver un compromis entre tous ces critères en privilégiant la cohésion, et ensuite, en essayant de minimiser le nombre de connexions. Selon Yourdon [You 75], le nombre d'erreurs est directement proportionnel au nombre de connexions. Nous pensons que la cohésion est le facteur le plus important car un module ayant une bonne cohésion (type abstrait ou fonctionnelle) constitue une brique de base cohérente et fiable. Si au contraire, on commence par privilégier le nombre de connexions, on peut déboucher sur une structure relativement simple, mais où la cohésion des modules atteint un niveau déplorable.

L'exemple suivant illustre ce point de vue.

Notre exemple montre la structure modulaire obtenue lorsque l'on décompose un projet selon un aspect fonctimmel-séquentiel (dans lequel on considère les fonctions au fur et à mesure de leur besoin), ou en fonction de types abstraits. L'énoncé du problème est le suivant: on désire réaliser un système qui lit des doruiées dans un fichier, les manipule et les stocke dans une structure de données en mémoire. Ensuite, on veut pouvoir effectuer un certain nombre d'interrogations sur cette structure de données. Les résultats des manipulations sont ensuite stockés dans un autre fichier, et on termine en détruisant la structure de données se trouvant en mémoire. Le traitement des données consiste principalement à lire des informations sous forme de chaînes de caractères et à les convertir ensuite sous forme de valeurs numé1iques. Les interrogations se résument à extraire des valeurs numériques de la structure de données et à les transformer en chaînes de caractères.

Construisons artificiellement deux décompositions en accord avec les deux approches désirées et analysons les. On commence d'abord par analyser et spécifier quelles sont les fonctions nécessaires lors des différentes parties (5) du programme:

1) Initialisation

Cette liste de fonctions peut très bien être le résultat d'une phase d'analyse où l'on applique des méthodes telles que SADT. On aboutit généralement à une liste de fonctions ordonnées séquentiellement. Ce type de liste a l'avantage de montrer quel est l'ensemble des besoins d'un projet, et de s'assurer que l'on oublie pas des fonctions importantes.

Intuitivement, cette liste, obtenue par une analyse que nous appellerons fonctionnelle-séquentielle, est relativement facile à établir et joue un rôle important, car elle permet de garantir la complétude des fonctions nécessaires.

A partir de cette liste, nous allons procéder à la conception de la structure modulaire selon nos deux approches.

La première modularisation consiste à regrouper en un même module les différentes fonctions nécessaires à chaque étape. Ce type de modularisation est simple à effectuer à partir du résultat des phases précédentes. Il privilégie l'aspect temporel, et il aboutit à un module par étape. Le graphe d'une telle décomposition est donné dans la figure 8.3.

constru Ire_ structure dorr ulro_slrucluro

Figure 8.3 : Décomposition modulaire selon une approche temporelle On remarque que ce graphe de dépendances est très simple, et qu'il comporte un minimum d'arcs, c'est-à-dire de dépendances inter-modulaires. Par contre, la cohésion des modules est réduite à une cohésion temporelle.

La deuxième modularisation consiste à regrouper les fonctions ayant des liens sémantiques assez fort pour constituer des types abstraits ou des ensembles de fonctions fortement liées (équivalentes à un type abstrait dégénéré). Ces modules auront donc une cohésion abstraite ou fonctionnelle. Le graphe de cette décomposition est schématisé dans la figure 8.4.

Oo remarque que le graphe de dépendances de cette deuxième modularisation comporte un plus grand nombre de liens, mais que la cohésion est maintenant acceptable à l'intérieur de chaque module, dont le nombre a d'ailleurs diminué. Ces exemples de structures modulaires sont quelque peu caricaturaux, mais ils ne sont pas tellement éloignés de la réalité.

Figure 8.4 : Décomposition modulaire selon une approche types abstraits La décomposition fonctio1U1elle-séquentielle est une conception top-down qui permet d'établir la liste des fonctions nécessaires à la réalisation du programme. Cette approche est nécessaire lors de la phase d'analyse, mais elle ne doit pas être prolongée à la phase de conception, car elle aboutit à une cohésion temporelle des modules. Il vaut mieu'X adopter alors une approche bottom-up, que l'on qualifiera de constructive, et qui pennet d'obtenir des modules ayant une forte cohésion.

Le deuxième graphe permet d'entrevoir un modèle général de graphe de dépendances. Les modules des couches du haut d'un graphe contiennent la logique de contrôle du programme pendant que les modules de bas niveau s'occupent des détails d'interfaçage. La structure de la partie haute du graphe est donc relativement simple, puisque les modules de ces couches consistent à ordonnancer et à contrôler l'exécution des différentes tâches. Généralement, les dernières couches sont constituées de modules de librairie propres au programme. Entre les couches de bas niveau et celles du haut du graphe règne une grande complexité, résultant du

problème suivant: comment passer des fonctions de haut niveau, définies dans les couches supérieures, aux fonctions de bas niveau des couches inférieures. Généralement, on réalise les fonctions de haut niveau les unes après les autres en créant des couches de modules intermédiaires. Ce sont ces couches qui induisent la plus grande complexité.

Nous pensons qu'un bon modèle de structure modulaire doit ressembler à un arbre composé de sous-arbres, qui eux-mêmes sont des arbres composés de sous-~rbres, et ainsi de suite. Un sous-arbre se compose de différentes couches de modules, entre lesquelles on permet le partage ou la réutilisation de modules. Nous introduisons pour cela, la notion de librairie locale qui permet d'avoir des modules utilisés par d'autres modules, sans que cela nuise à l'indépendance du sous-arbre. La figure 8.5 schématise un tel modèle.

Figure 8.5 : Modèle de structure modulaire Caractéristiques d'un graphe

Nous allons approfondir certaines caractéristiques d'un graphe qui nous montreront de nouveaux aspects d'un graphe. En particulier, nous allons regarder différentes représentations d'un graphe, et les différents types d'arcs entre les noeuds. Ces différents aspects nous pennettront de mieux saisir la notion de graphe des dépendances et d'ouvrir de nouveaux champs d'investigation dans l'évaluation de la complexité de tels graphes.

La représentation d'un graphe de dépendances peut s'effectuer soit par la donnée de la matrice de dépendances, soit par un graphique. On peut se poser la question suivante: la matrice ou le graphe sont-ils uniques pour un projet donné? La réponse est non, car la détermination du niveau d'un module n'est pas forcément unique. En effet, la plupart des modules peuvent s'insérer dans un niveau parmi plusieurs possibles. La figure 8.6 nous donne un exemple très simple de deux graphes différents dérivés des mêmes dépendances modulaires.

@0

Figure 8.6 : Différents graphes d'un même programme

Cet exemple nous montre 3 graphes (parmi 6 possibles) construits avec 7 noeuds seulement. Plus un programme se compose d'un grand nombre de modules, plus le nombre de représentations possibles est grand. Nous allons nous contenter d'examiner deux représentations, obtenues par les approches top-down et bottom-up.

L'approche top-down consiste à attribuer le niveau maximal possible de chaque noeud. Le premier graphe montré dans la figure 8.6 est un graphe top-down. Pour trouver le niveau de chaque noeud, on commence par la racine (premier niveau) et l'on regarde quels sont les noeuds qui lui sont reliés par un arc de base. Un arc est dit de base s'il est indispensable à la conservation d'un chemin dans le graphe. Nous verrons plus loin les autres types d'arcs. Donc, tous les noeuds reliés par un arc de base à la racine constituent le deuxième niveau. On recommence de la même manière en constituant le troisième niveau de tous les noeuds reliés à un noeud du deuxième niveau par un arc de base. Et ainsi de suite pour tous les autres niveaux. Cette approche est celle qui se rapproc.he le plus du modèle précédent d'arbre et de sous-arbres, mais elle a Je principal inconvénient de mettre des modules de bas niveau dans des couches de haut niveau. En effet, si un module de haut niveau appelle un module de bas niveau, et que celui-ci n'est pas appelé par aucun autre module, alors il se

retrouve au niveau juste inférieur à celui du module appelant. Sur un graphe complexe, par exemple le graphe de l'outil EM2, on ne se rend plus compte de la structure propre à la partie haute du graphe, qui renferme la logique de contrôle du programme.

L'approche bottom-up consiste à attribuer le niveau minimal de chaque noeud. Le troisième graphe contenu dans la figure 8.6 a été obtenu selon cette approche. Contrairement à la méthode précédente, on commence par rechercher tous les noeuds de la dernière couche, c'est-à-dire tous ceux qui n'ont pas de liens inférieurs. Ensuite, on cherche tous ceux qui sont reliés avec un noeud de cette dernière couche, en faisant attention qu'il n'y aient pas de chemins possibles entre eux. Et on recommence de la même manière pour les couches supérieures. Cette approche a le grand avantage de laisser les modules de bas niveau, tels que les modules de librairie, dans les couches les plus basses.

Un certain nombre de remarques s'imposent. La première consiste à relativiser l'importance du niveau d'un module. Ainsi dans la constitution de groupes de modules, on ne se limitera pas à des modules du même niveau. Une autre remarque porte sur l'obtention d'un graphe représentant les modules propres à un projet. Nous proposons de commencer par calculer le graphe d'un projet par l'approche bottom-up, d'en enlever tous les modules des couches de bas niveau, considérés comme des modules de librairie, et ensuite de restructurer ce graphe selon l'approche top-down. C'est selon cette approche que le graphe comporte le plus grand nombre d'arcs de base reliant deux noeuds appartenant à des niveaux consécutifs. Graphiquement, cela donne le graphe le plus simple.

On peut remarquer aussi que les noeuds se trouvant au même niveau selon les deux approches correspondent aux noeuds constituant la plus longue branche du graphe.

Considérons maintenant les différents types d'arcs d'un graphe.

Nous distinguons trois types d'arcs: les arcs directs, les arcs de base, et les arcs indirects . Un arc est direct (D) entre les noeuds x et y s'il existe effectivement un lien de dépendance entre des objets de ces deux modules.

Un arc est dit de base (B) si c'est un arc direct et s'il est indispensable à la conservation d'un chemin du graphe. Un arc est indirect (1) entre les noeuds x et y s'il n'y a pas de dépendance directe entre les modules concernés, mais qu'il existe un chemin entre ces noeuds empruntant d'autres noeuds intermédiaires. En fait, les arcs indirects sont obtenus par application de la transitivité aux arcs directs du graphe. La figure 8.7 montre un graphe comportant ces différents types d'arcs.

Les arcs de base forment un graphe · de base qui est unique.

L'ensemble des arcs B et D correspond à l'ensemble des dépendances existant entre les modules. Les arcs I, en plus des précédents, permettent

de former un graphe d'atteignabilité, composé de tous les chemins possibles entre les noeuds. On remarque que les arcs D relient toujours des noeuds se trouvant sur des niveaux non consécutifs.

Figure 8.7 : Différents types d'arcs d'un graphe

Un des objectifs de cette étude est d'arriver à calquer un graphe sur 1e modèle d'arbre et de sous-arbres. Pour cela, il faut grouper les modules pour former des sous-arbres indépendants. Cela n'est pas souvent possible, en raison d'arcs malvenus. Nous proposons d'essayer de détecter ces arcs malvenus qui créent des dépendances entre des sous-arbres presque indépendants.

Complexité d'un graphe de dépendances

Nous proposons d'évnluer la complexité de la structure du graphe des dépendances modulaires d'un programme à l'aide des mesures suivantes:

- le nombre de sommets - le nombre d'arcs - la densité

- le rapport de cohésion

- la distance par rapport à un modèle de base.

Le nombre de sommets et le nombre d'arcs permettent de donner une idée de l'importance de la structure considérée. La théorie des graphes nous sert de support pour défmir les autres caractéristiques; pour plus de précision se référer à [Min 86) et [Gon 79].

La densité permet de donner une mesure de la complexité d'un graphe. Elle est égale au rapport entre le nombre d'arcs de ce graphe (M)

et le nombre d'arcs du graphe complet ayant le même nombre de sommets (N). Un graphe est dit complet si, pour toute paire de sommets (ij), il existe au moins un arc de la foIJDe (ij) ou (j,i). Dans le cas d'un graphe

. 'Id., , l ' M onente, a ens1te est ega e a ~ , , .

Le rapport de cohésion correspond à évaluer le pourcentage de modules ayant une cohésion abstraite ou fonctionnelle, par rapport au nombre total de modules.

Le graphe des dépendances modulaires d'un logiciel est, généralement, un graphe complexe. Pou.r définir une mesure de sa compJexité, nous avons décidé d'évaluer la distance séparant ce graphe d'un graphe simple pris comme modèle de base. Notre modèle de base est une arborescence. Une arborescence de racine r est un arbre (graphe connexe sans cycles) tel que pour tout sommet j, il existe un chemin allant der à j. Une première évaluation simple consiste à calculer le nombre d'arcs (NA) qui séparent le graphe du modèle de base. Etant donné que le nombre d'arcs d'une arborescence est égal au nombre de sommets (de l'arborescence) moins un (pour la racine), le nombre d'arcs (NA) qui nous intéresse est égal à :

NA= nombre d'arcs du graphe - (nombre sommets -1).

Cette valeur permet d'obtenir une évaluation de la distance absolue qui sépare le graphe d'une arborescence possédant le même nombre de sommets.

Une deuxième manière d'évaluer une telle distance consiste à effectuer une réduction du graphe pour obtenir le modèle de base. La réduction consiste à grouper des sommets du graphe entre eux et grouper les arcs correspondant à ces sommets de telle façon que le graphe obtenu soit une arborescence (figure 8.8).

Les critères de regroupement des modules sont fonction des dépendances des modules. Le premier critère est fonction du couplage. On commence par gouper les modules ayant un couplage du type constructeur. Un deuxième critère consiste à regarder quels sont les modules qui sont utilisés conjointement. Pour ce faire, on calcule la matrice des dépendances modulaires à partir du graphe des dépendances.

C'est une matrice carrée (N,N), où N représente le nombre de sommets, que l'on peut mettre sous forme triangulaire en ordonnant les modules suivant leur niveau dans le graphe (figure 8.9).

Remarque: le premier critère de regroupement fait intervenir la notion de groupe de modules. Conceptuellement, cette notion permet de définir Wl

nouveau niveau d'abstraction entre Je niveau module et le niveau projet

Graphe des dépendances modulaires

Réduction

Arborescence

Figure 8.8 : Réduction d'un graphe en une arborescence

niveau O

niveau

niveau 2

Graphe des dépendances

' ' 1

1- -~ .§l ~ !;> _ ~ -<?~ ~

_;:

_<.!_~

~J~J---L---

b: c:x:. x:.

1 1

d1X1 • • • 1

~-~-~---~---e•. •X •• •

f:x:x x .:

g: :

X

x:

h:.:. X x:

Matrice des dépendances

Figure 8.9 : Matrice des dépendances

Ensuite, on extrait la sous-matrice propre à chaque niveau et on groupe les modules qui ont des dépendances communes, que ce soit des dépendances avec des modules d'un niveau supérieur ou inférieur. On remplace, dans la matrice, les modules groupés par un seul élément. On répète cette opération jusqu'à ce que les lignes de la matrice des dépendances ne possèdent plus qu'un seul élément significatif (exceptée la première ligne), c'est-à-dire que le graphe consiste en une arborescence.

Certains arcs (les arcs malvenus) empêchent le regroupement en établissant un lien entre deux groupes presque indépendants. Plus généralement, il arrive que l'on ne puisse pas dire que tel module appartient logiquement à telle branche plutôt qu'à telle autre en raison de la présence de liens le reliant à ces deux branches. Nous résolvons ce problème en détectant les liens les plus logiques eLen supprimant les autres du graphe, tout en les signalant dans la mesure de complexité. Leur détection s'effectue en calculant le nombre de chemins différents existant entre les noeuds de chaque branche et ce module. On l'attribue à la branche qui présente le plus grand nombre de-chemins. Evidemment, c'est une mesure statistique qui est significative lorsque le module appartient logiquement à une branche et qu'il y a seulement un ou deux arcs malvenus le reliant à une autre branche. Mais dans le cas où il y a autant de chemins le reliant à chaque sous-arbre, alors il faut grouper tous les modules concernés.

Le degré de regroupement indique la complexité de la structure. Si l'on n'a pas besoin de grouper de modules pour obtenir une arborescence, alors on est dans le cas idéal (peu réaliste). Si un regroupement des modules selon le critère de couplage suffit, alors on est dans un cas presque idéal (déjà plus réaliste). Si l'on applique une métltode de conception basée sur les types abstraits, alors on devrait produire un programme dont la structure appartient à cette catégorie. L'autre cas extrême est celui où l'on est obligé de grouper tous les modules en un seul paquet.

Pour mesurer l'importance de la réduction, on définit deux rapports :

nombre de sommets de l'arborescence nombre de sommets du graphe initial nombre d'arcs de l'arborescence nombre d'arcs du graphe initial

Ces rapports vont prendre leurs valeurs dans l'intervalle [0,1], 1 étant la valeur maximale signifiant que le graphe initial est déjà une arborescence.

8.3 RESTRUCTURATION

Le graphe des dépendances modulaires d'un grand projet présente généralement une structure complexe. La complexité de cette structure a encore tendance à augmenter avec le temps. La principale raison en est la suivante: le logiciel subit des modifications par les diverses opérations de

Le graphe des dépendances modulaires d'un grand projet présente généralement une structure complexe. La complexité de cette structure a encore tendance à augmenter avec le temps. La principale raison en est la suivante: le logiciel subit des modifications par les diverses opérations de