Refactoring des dépendances | 1 vote(s)
Par Emmanuel Deloget, vendredi 13 juillet 2007 à 21:38 :: Architecture, divers :: permalien #92
Tags: blogs, design pattern, principe POO, refactoring, SRP, UML
Récemment, je suis tombé sur le site de Jason Gorman, consultant reconnu pour son expertise de l'architecture logicielle et des méthodes agiles[1]. Et sur son blog, j'ai découvert un test d'architecture logicielle qu'il utilise pour s'assurer de la qualité des aspirant architectes qui souhaitent travailler avec lui.
Notes
[1] et avec lequel je suis solidaire lorsqu'il donne sa vision de ce qu'est un architecte logiciel
Le test, tel que je le comprends, est simple: soit un certain nombre de classes réparties dans plusieurs packages. Si votre but est d'améliorer la maintenabilité de la solution, quel refactoring appliqueriez vous, pourquoi et quel serait le ou les bénéfices apportés par ces changements ?

Il y n'a qu'une seule limite imposée : les classes et les dépendances entres les classes ne doivent pas être modifiée - le but de cette restriction est de s'assurer que le comportement du système ne sera pas modifié. Histoire d'être honnête avec vous, je ne sais pas si cela implique qu'aucune classe ne peut être ajoutée à ce modèle - et sieur Gorman n'a pas encore daigné répondre à mon mail-réponse[1]. Soit. Je prends donc le parti de n'autoriser aucun ajout de classes, puisque c'est la règle que j'ai suivi lorsque j'ai proposé ma solution.
En soit, il s'agit là d'un problème intéressant - il y a plusieurs solutions possibles, mais toutes les solutions ne sont pas équivalentes en terme de maintenabilité.
On peut par exemple supprimer P3 en déplaçant la classe G dans P1 et H dans P2. On supprime ainsi un package et deux relations de dépendances. Et un package de moins à gérer, c'est toujours ça. Sauf que. Qui a dit que c'était une bonne chose ? Etant donné que nous avons juste déplacé des fichiers, la somme de code à maintenir est toujours la même. En quoi supprimer un lien de dépendance peut-il être bénéfique à un projet ? Est-ce toujours une bonne chose de réduire les dépendances entre les modules ?
Il y a deux réponses à cette question. Si par module vous entendez "classes" - et on vous pardonnera votre abus de langage - alors oui, supprimer les dépendances d'une classe envers les autres est dans la plupart des cas une bonne chose, à condition que votre architecture continue d'être cohérente et que votre modification ne signifie pas que les classes modifiées ne respectent plus le principe de responsabilité unique. La raison en est simple : en supprimant une dépendance entre deux classes, j'ai introduit un découplage supplémentaire qui me permet d'utiliser ces deux classes librement, sans avoir à me référer à l'autre lorsque j'utilise l'une. Le résultat est une proposition d'ouverture du code supplémentaire (une proposition d'ouverture car je peux peut-être utiliser ces classes dans un autre contexte, mais une proposition seulement car rien ne m'oblige à le faire).
Maintenant, si vous êtes à cheval sur la terminologie, vous employez certainement le mot "module" pour parler d'un package (au sens UML, c'est à dire un groupe de classes et de sous-packages). Dans ce cas, la réponse est "la plupart du temps, non, mais au final ça dépends surtout de conditions externes". Ainsi, si le module que vous souhaitez supprimer est référencé par d'autres modules, le problème risque d'être un brin épineux. Déplacer les classes et les services définis dans ce module peut créer d'autres dépendances qui seront peut être encore plus difficiles à gérer. Au niveau maintenance, le résultat peut être catastrophique, comme il peut être bon.
Dans ce cas, quelles sont les questions que je dois me poser avant d'effectuer ce refactoring ? Et nous tombons là dans le domaine de l'opinion, car je n'ai que peu de sources[2] pour corréler ce qui va suivre.
D'après mon expérience personnelle, il convient de se poser trois questions - l'ordre dans lequel je vous les livre n'est pas leur ordre d'importance.
- Que représente ce module dont je souhaite me débarrasser ?
- Est-ce que ça a du sens de déplacer telle ou telle classe de ce module dans tel ou tel autre module ?
- Qu'est-ce que j'y gagne, en fin de compte ?
La réponse à ses questions passe d'abord par un audit du code existant - qui fait quoi, pourquoi, comment, et quels sont les liens entre les classes de ce module à supprimer (appelons-le module source, pour plus de simplicité; les classes du module source sont alors les classes sources) ?
Ce que je considère comme étant un bon design est assez difficile à expliquer, mais tâchons de le faire quand même. Pour moi, un bon design respecte les 5 principes fondamentaux de l'architecture objet (déjà exposés sur ce blog dans des billets antérieurs, je ne reviens donc pas dessus). De plus, ce qui fait l'unité d'un module est la notion de domaine sémantique - les classes et services sont donc groupés par rapport à la fonctionnalité qu'ils offrent, en respectant bien sûr les niveaux d'abstraction. J'y ajoute généralement les classes qui ne sont pas accessibles à l'utilisateur - les détails d'implémentation.
Dans une telle organisation, une dépendance d'un module vers un autre a du sens - il s'agit pour un module d'utiliser un service particulier dans un domaine particulier, il n'a donc pas sa place ailleurs que là ou il se trouve. Si le petit jeu de Jason Gorman utilise les même concepts d'architecture, alors je suis au regret de lui dire que je ne peux rien faire - dans le cas contraire cela me forcerait à détruire l'unité des packages. Dans tous les cas, sans cette vision de domaine sémantique, je suis coincé - comment puis-je m'assurer que le refactoring sera viable si je n'ai aucune idée de ce que font les classes ?
Une fois définie norte manière de voir les choses, il devient plus aisé de répondre aux trois questions que j'ai posé ci-dessus:
- Que représente ce module dont je souhaite me débarrasser ?
Si le module source n'a pas de sémantique propre - ce qui arrive bien plus souvent qu'on ne souhaite l'admettre - c'est qu'il n'a pas lieu d'exister. Les classes et les services n'ont que peu de liens entre eux, et il est de fait plus judicieux de casser ce module en plusieurs modules ayant une véritable sémantique. Si un ou plusieurs modules existant partagent des sémantiques similaires, on peut tenter d'intégrer nos nouveaux composants dans ces modules - ce qui m'amène au point suivant. - Est-ce que ça a du sens de déplacer telle ou telle classe de ce module dans tel ou tel autre module ?
Et bien, ça dépends encore une fois de la sémantique du package cible - même si on s'aperçoit qu'il est possible de contourner légèrement cette notion de sémantique. Si la sémantique de ma classe source s'inscrit dans celle de mon module cible, alors la réponse est "oui, dans la plupart des cas", dans la mesure ou le design global continue de respecter les qualités que j'ai énoncé plus haut. Si les sémantiques ne correspondent pas, pas de panique - est-ce qu'on peut considérer la classe source comme étant un détail d'implémentation du module cible ? C'est à dire, est-ce que une fois l'intégration effectuée, cette classe source fera partie du protocole du module cible ? Si vous répondez oui à la première question et non à la seconde, alors je dirais qu'il est conseillé de transférer la classe source dans le module cible - et même, dans un sous-package privé du module cible. Si votre réponse est différente, c'est que cette classe est bien là ou elle est, ou plus exactement qu'elle ne serait pas bien là ou vous voulez la mettre. - Qu'est-ce que j'y gagne, en fin de compte ?
La notion de gain, lorsqu'on parle d'architecture logicielle, se doit d'être interprétée avec la plus grande prudence. Un gain de productivité à court terme ne signifie pas qu'au final, le projet aura bénéficier de la modification - et bien souvent, c'est même l'inverse qui se produit. La question de la maintenance soit être soulevée, car c'est bien souvent dans les phases de maintenance (une fois que le cycle de développement produit arrive à son terme) que les modifications d'architecture se font le plus sentir: une mauvaise modification entrainera un surcout dans la correction des problèmes, tandis qu'une bonne modification aura tendance à diminuer ces couts, voire à diminuer le nombre des problèmes. Trouver une réponse à cette question est donc primordial, et pourtant c'est probablement la question à laquelle il est le plus difficile de répondre. Pourquoi ? Tout simplement parce nous ne connaissons pas à l'heure actuelle de métrique qui nous permette de calculer la maintenabilité d'un projet à priori : la seule chose que l'on sait faire, c'est chiffrer les couts de maintenance à postériori. C'est donc là qu'interviennent les qualités d'un architecte logiciel - et notamment son aptitude à juger de la qualité d'une architecture.
Pour conclure, je vous invite à me proposer vos solutions au problème posé par Jason Gorman. Je vous fait part des solutions que je lui ait proposé - mais pour lesquelles je n'ai aucun retour - afin de vous servir de base de discussion. Je ne prétends pas que ces solutions soient correctes, ou même que mon interprétation de leurs qualités soit correcte - à vous de juger !
Commentaires
1. Le vendredi 13 juillet 2007 à 22:14, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire