Valider et corriger une architecture objet, première partie | 0 vote(s)
Par Emmanuel Deloget, vendredi 1 février 2008 à 02:00 :: Architecture Orientée Objet :: permalien #112
Tags: encapsulation, OCP, principe POO, SRP
La validation d'une architecture orientée objet est une étape importante dans le cadre de la mise en route d'un projet. Il s'agit de vérifier si l'architecture imaginée "tient debout", c'est à dire si d'une part elle réponds au besoin et si d'autre part elle est, disons, "suffisamment correcte". J'emploie à dessein une expression vague car la plupart du temps nous n'avons qu'une idée confuse de ce qui pourrait être une architecture "suffisamment bien" - nous n'avons pas vraiment de métrique à nous raccrocher[1].
Cet article est en deux parties. La seconde partie sera publiée la semaine prochaine.
Notes
[1] Il existe bien des métriques quantitatives (nombre de classes, nombres de méthodes par classes, nombre de liens entre les classes, etc) mais ces métriques ne nous donne que peu d'indications quand à la qualité de l'architecture objet réalisée.
Si nous remontons un peu dans le temps, les quelques premiers mois de vie de ce blog on été consacrés à la description (plus ou moins réussie...) des 5 principes majeurs de la POO :
- le principe "ouvert/fermé",
- le principe de substitution de Liskov,
- le principe de responsabilité unique,
- le principe de ségrégation des interface
- enfin, le principe d'inversion de dépendance.
A ces 5 piliers, j'avais ajouté ce que je considère personnellement comme un méta-principe - le principe d'encapsulation.
Avec le présent article, je vous propose de revenir sur ces principes et de voir comment on peut vérifier leur application et ainsi obtenir des métriques qualitatives qui nous donnent une idée plus précise de la validité d'une architecture objet. Nous verrons aussi comment on peut revenir sur certains problèmes mis en lumière par une analyse afin de les corriger.
Avant de commencer...
Les paragraphes ci-dessous sont composés de plusieurs sections:
- le titre - le nom de principe en anglais (sauf pour le principe d'encapsulation)
- sa formulation usuelle (en anglais aussi, sauf pour le principe d'encapsulation). Attention, ce n'est pas forcément la formulation utilisée par son découvreur.
- une courte discussion sur le sujet
- les symptômes tendant à montrer que le principe n'est pas appliqués
- les corrections qu'il est peut-être possibles d'effectuer afin de pouvoir appliquer le principe.
Principe d'encapsulation
On encapsule des comportements, pas des propriétés
Pourquoi : Attention - même si mon article originel est un peu trop extrémiste, la formulation ci-dessus n'en reste pas moins correcte. Cela ne signifie pas pour autant que les propriétés ne doivent pas être encapsulée, ou qu'il est interdit à une classe de définir des propriétés. L'idée de base est qu'un objet définit principalement un ensemble de comportements qui sont basés sur ses propriétés intrinsèques. Ce principe est similaire au principe de dissimulation d'information (information hiding principle) décrit par David Parnas[1] et discuté par Bertrand Meyer[2].
Symptômes : Une classe exporte ses propriétés (via des getters et des setters) mais ne définit pas ou peu de comportement pour traiter ces propriétés. De fait, la plupart des traitements sont effectués hors de l'objet, en conséquence de quoi de larges portions de code vont être dupliquées, ce qui entrainera des difficultés de maintenance (par exemple, une certaine rigidité ou fragilité du code - voire les deux ensembles).
Correction : Dans la plupart des cas, il s'agit d'appliquer quelques refactoring[3] connus :
- Extract Method pour créer des méthodes à partir du code qui utilise les propriétés de notre classe
- Move Method pour transférer ces nouvelles méthodes dans notre classe
Single Responsability Principle
There should never be more than one reason for a class to change
Pourquoi : Si une classe a plusieurs responsabilités, un changement dans l'une de ses responsabilités peut entrainer un changement chez tous les clients de cette classe, même si ces clients ne se servent pas de cette responsabilité. De plus, donner plusieurs responsabilités à une classe introduit des couplages forts qui ne sont pas souhaité (par exemple : l'utilisation d'une classe image avec une méthode read et une méthode display nécessite une dépendance avec un module d'affichage qui n'est pas nécessairement souhaitable dans le cas du développement d'un logiciel sans interface graphique).
Symptômes : Le cas le plus évident est celui - bien connu - de la God Class : une classe si pourvue en comportements différents qu'elle en devient une pièce centrale de l'application. N'importe quelle fonction finit, à un moment ou à un autre, par se servir de l'un des services offert par la God Class.
Moins trivial, le cas des classes "manager". J'appelle classe "manager" une classe dont on ne sait pas définir le nom correctement. Au final, on considère qu'elle gère ceci ou cela. Dans 99% des cas, cette confusion vient du fait que les responsabilités de la classe ne sont pas correctement définies et sont en trop grand nombre. L'exemple typique est le cas du ressource_manager chers aux programmeurs de jeux vidéo, qui a pour mission :
- de charger des ressources en mémoire
- de créer des nouvelles ressources
- de stocker des ressources dans une liste de ressources accessibles
- de déterminer quand ces ressources doivent être libérées.
Correction : Corriger une architecture n'appliquant pas le SRP est d'une complexité relative - la plupart du temps, on applique tout simplement le refactoring Extract Class. Cependant, il est nécessaire de découvrir les différentes responsabilités de la classe avant de pouvoir appliquer cette procédure. La plupart du temps, je procède par regroupement de fonctionnalités, puis extraction de ces fonctionnalités dans une classe à laquelle je tente de donner un nom[4] correct. Si je reprends le ressource_manager précédemment cité, j'y entrevois au moins un loader, une factory et une collection.
"Open/Closed" Principle
Software should be open for extension, but closed for modification.
Pourquoi : dans un logiciel, certains composant sont plus stratégiques que d'autres. Bien évidemment, lorsqu'on parle de composants stratégiques, ont ne fait pas nécessairement référence aux fonctionnalités du logiciel mais aux risques liés au développement de ces composants. Lorsqu'on identifie un composant réutilisable, il est important de bien penser son interface afin de maximiser les possibilité de réutilisation - par héritage ou par composition. Une interface intelligente permettra de limiter les modifications à l'intérieur du composant lors de la maintenance de celui-ci, tout en autorisant son extension grâce aux mécanismes sus-cités. Dans le cas contraire, chaque réutilisation entraîne une modification du composant, ce qui réduit la maintenabilité du code et donc, in fine, sa fiabilité.
Symptômes : Plusieurs cas peuvent être distingués.
- Une classe a besoin de connaître le type d'une ou plusieurs autres classes afin d'effectuer un traitement et ajouter une nouvelle classe à l'application entraine la modification de ce code; ou, de manière plus générale, une séquence d'instruction dépends de la connaissance d'un type particulier.
- Une classe dérivée a un comportement qui n'est pas similaire à celui de sa classe mère.
- Une classe a un rôle central dans l'application (c'est à dire qu'elle a beaucoup de clients). Chaque client appelle une série de méthode différente. L'ajout d'un client force l'écriture d'une nouvelle méthode dans la classe.
Correction : En reprenant les symptômes ci-dessus.
- Le design patterns Strategy ou - de manière similaire - l'application du refactoring Replace Type Code with State/Strategy est une solution possible à ce problème, de même que les refactorings Replace Type Code with Subclasses ou Replace Type Code with Class.
- La plupart du temps, c'est dans la notion d'héritage que se cache le problème : si une classe dérivée n'a pas le même comportement que sa classe parent, c'est que la relation "is-A" est biaisée. Ce problème sera traité plus longuement dans la discussion sur le principe de substitution de Liskov qui apparaîtra dans le prochain billet de la série. Au niveau de OCP, ce qu'on remarque c'est que le code client a besoin de connaitre la classe qu'il utilise afin de pouvoir ajuster ses propres réactions. Si, dans le principe, on est proche du cas précédent, il n'en reste pas moins que la correction est différente : dans la plupart des cas, il faut casser la relation d'héritage - avec tout ce que cela implique comme modification au niveau de l'architecture.
- Il y a un problème d'abstraction : clairement, l'interface de la classe n'est pas correctement définie, ou il manque un niveau d'indirection. Dans la plupart des cas, redéfinir une abstraction n'est pas une mince affaire. Ceci dit, si les problèmes sont très localisés, on peut toujours tenter de donner des coups de bistouris secs (Extract Method, Add Parameter, Remove Parameter et d'autres refactoring peuvent être d'une grande utilité ici). L'idée est de fournir une abstraction qui fournit une série de services élémentaires. Par la suite, on peut encapsuler cette abstraction dans plusieurs classes qui fournissent les services souhaités aux clients par composition. L'ajout d'un nouveau client se traduit alors par l'ajout d'une nouvelle classe.
A venir
Le prochain article (en cours d'écriture - promis, vous y aurez droit la semaine prochaine) aura pour sujet les trois autres principes (LSP, DIP et ISP).
Notes
[1] D.L. Parnas, On the criteria to be used in decomposing systems into modules, Communication of the ACM, Dec. 1972, Vol. 15, Number 12; Un PDF est disponible ici
[2] Bertrand Meyer, Object Oriented Software Construction, Prentice Hall, 1988
[3] Martin Fowler, Refactoring - Improving the Design of Existing Code, Addison Wesley, ISBN-13: 978-0201485677
[4] oui; parce qu'un objet est une chose - il lui est donc appliquer un nom, par opposition à une fonction qui est référencée par un verbe. Voir Execution in the Kingdom of Nouns pour en rire un peu.
Commentaires
1. Le lundi 4 février 2008 à 05:02, par Armand
2. Le lundi 4 février 2008 à 10:47, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire