29 fév. 2008

Valider et corriger une architecture objet, seconde partie

Dans la première partie, nous avons vu comment vérifier qu'une architecture validait les principes d'encapsulation, SRP et OCP. Dans cet article, nous allons étudier l'incidence des trois autres principes principaux - LSP, ISP et DIP - sur une architecture orientée objet.

Liskov Substitution Principle

Functions that use pointers or references to base classes shall be able to use objects of derived classes without knowing it.

Pourquoi : Il est courant d'utiliser les facilités offertes par l'héritage afin de simplifier l'extension d'un framework de classes et de lui ajouter de nouvelles fonctionnalités. Cependant, certaines précautions doivent être prises, car il est important que les classes nouvellement définies n'ai pas un impact négtif sur le code déjà écrit par ailleurs.

Symptômes : Plusieurs symptômes peuvent montrer qu'un design ne respecte pas ce principe. Dans une fonction f() utilisant une instance d'une classe de base B, je dois tester le type exact de la classe afin d'effectuer un traitement particulier pour certains sous-types de B; ou la redéfinition de méthodes virtuelles dans une classe dérivée D impose des restrictions qui ne sont pas présentes dans sa classe de base B, rendant impossible la validation des entrées et des sorties de cette méthode; ou la redéfinition d'une méthode virtuelle dans D change le sens de l'interface de sa classe mère D.

Correction : Plusieurs types de corrections sont possibles, selon les cas. Les deux corrections les plus courantes consistent à modifier l'interface de la classe de base - dans le cas ou cette modification est possible et si elle n'entraîne pas le non respect d'autres principes de design OO - ou a casser le lien d'héritage entre la classe dérivée et la classe mère. C'est notamment ce dernier choix qu'on préférera si on abuse de la notion d'héritage afin de faciliter la réutilisation de code. Plus généralement, et c'est le point du LSP, la notion d'héritage dans une architecture objet est liée à la notion de comportement vu du client.

A ma connaissance (partielle, limitée) il n'existe pas de refactoring décrivant la procédure à suivre pour casser une relation d'héritage. Selon les cas, cela peut être plus ou moins complexe. Puisque l'idée est de réutiliser du code, il ne semble pas illogique de recréer la classe dérivée (avec une interface similaire si-besoin) et d'utiliser la notion de composition afin d'importer les comportements utiles. Toutefois, cette opération ne peut se faire tant que l'on a pas défini une interface claire pour accéder à ces comportements.

Interface Segregation Principle

Clients should not be forced to depend on interfaces that they do not use.

Pourquoi : La dépendance envers des interfaces multiples a plusieurs conséquences. Premièrement, elle introduit des couplages forts, qui peuvent nuire à la réutilisation des classes et fonctions définies. Deuxièmement, elle est souvent un symptôme que quelque chose d'autre ne va pas : dans la plupart des cas, cette dépendance masque un subtile problème lié au non respect du principe de substitution de Liskov.

Symptômes : Ils sont difficiles à détecter. Dans de nombreux cas, un problème de dépendance se pose : par exemple, une classe D hérite de l'interface de B mais n'utilise pas son interface - dans ce cas, il est probable qu'on en arrive à créer une classe qui hériterait ensuite de D mais qui n'aurait aucun intérêt à avoir une quelconque relation avec B. L'interface de D est dite polluée par l'interface de B. Cela se produit notamment lorsqu'on force une relation de hiérarchie simple (une classe hérite au plus d'une seule autre classe).

Correction : la notion d'interface est une clef de voûte de la programmation orientée objet. Certains langages la mettent en avant (java, C#), d'autre moins (C++). Cependant, les langages orientés objets offrent presque tous un moyen de les implémenter (que ce soit avec un mot clef spécifique ou en utilisant des mécanismes plus génériques, telles que les classes abstraites pures en C++). Une fois les interfaces correctement définies, il ne nous reste le plus souvent qu'à casser les relations d'héritage qui sont contre-productives et à la remplacer par un héritage multiple judicieux (dans les langages qui le permettent) ou par un héritage d'interfaces (idem, dans les langages qui le permettent).

Dependency Inversion Principle

1. High level modules should not depend upon low level modules. Both should depend upon abstractions.
2. Abstractions should not depend upon details. Details should depend upon abstractions.

Pourquoi : Se baser sur des entités concrètes pour créer une architecture d'application, c'est prendre le risque de voir des informations importantes changer rapidement et de passer plus de temps en maintenance du code déjà écrit en lieu et place du développement de nouvelles fonctionnalités. Le DIP a pour idée maîtresse de limiter l'utilisation de détails (sous entendu: d'implémentation) dans les interfaces entre les différents modules de manière à autoriser un design plus modulaire et à simplifier la maintenance globale de l'application : le code, plus générique, est aussi plus simple et plus robuste.

Symptômes : Un module (une classe) intelligent(e) se base sur un ou plusieurs modules (classes) de bas niveau en exploitant directement la connaissance du fonctionnement de ces derniers. De fait,

  1. un changement dans les modules bas niveau a un impact sur les modules à plus haut niveau.
  2. il est impossible d'utiliser le module haut niveau sans ses modules bas niveau.

Correction : Le mot clef pour la correction de problèmes liés au non respect de ce principe est "abstraction". Les abstractions ont un avantage indéniable par rapport aux entités plus concrètes : leurs propriétés et leur comportement change peu. De fait, elle font de bonnes interfaces entre les modules.

Dans un sens, cette notion d'abstraction sous-tend tous les autres principes d'architecture objet. Sans abstraction, pas de OCP - donc pas de LSP. La notion d'interface devient elle-même biaisée, donc pas de ISP. L'abstraction est aussi à la base de l'encapsulation. La notion de responsabilité devient floue, donc on perd aussi le SRP. Bref : on perd tout.

Il ne faut pas confondre abstraction et classe abstraite : la notion de classe abstraite n'est pas suffisante dans ce cas. Une abstraction réponds à une question : "qu'est-ce que c'est que..." ? Par exemple : qu'est-ce qu'un modem ? qu'est-ce qu'un scanner ? Qu'est-ce qu'un gestionnaire de mémoire ? En fonction de la réponse, on peut en déduire son comportement et ses propriétés - bref, son interface. Mais dès lors que cette interface fait apparaître des subtilités liées à l'implémentation, on perd un niveau d'abstraction et on rend plus difficile l'intégration du module dans le projet. Toutefois, les classes abstraites en C++ sont le seul moyen d'implémenter des interfaces (au sens C# ou Java). Ces interfaces peuvent être des alliées utiles pour corriger le problème décrit.

L'idée de base de la correction est d'ajouter des couches d'interfaces entre le module appelant et le module appelé. Dans cet article (pdf), R.C Martin montre que sans cette couche additionnelle, les dépendances sont transitives. En transformant une dépendance envers un module bas niveau en une dépendance envers une abstraction, on généralise un concept et et casse cette transitivité.

Conclusion

Voilà. Cette étude en deux partie est terminée - elle a pris plus de temps que prévu, désolé. J'espère que cela vous permettra de mieux comprendre votre propre code et de mieux palier à ses défauts. Avec un peu d'expérience, on s'aperçoit que les problèmes d'architecture sont relativement faciles à détecter, même si ils peuvent être complexes à corriger. L'idéal reste donc de bien garder les quelques principes d'architecture objet en tête au moment même de faire le design de l'application, de manière à ne pas avoir à corriger ces problèmes plus tard.

Commentaires

1. Le dimanche, septembre 15 2013, 10:30 par toto

"dans une function f()" -> une fonction
"une problème de dépendance" -> un problème de dépendance

Je cherchais des articles sur SOLID, et je pense que celui-ci va finir dans mes favoris pendant un certain temps ;)

2. Le dimanche, septembre 15 2013, 15:46 par Emmanuel Deloget

Merci pour ces corrections !

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet