Principes avancés de conception objet Jean-Jacques LE COZ
Plan Introduction Gestion des évolutions et des dépendances entre classes Organisation de l'application en modules Gestion de la stabilité de l'application
Introduction
Principes de base Encapsuler ce qui varie Programmer par interface plutôt que par implémentation Chaque classe ne doit avoir qu'une seule raison de changer Un concept est éligible à devenir une classe si il correspond à du comportement ou à une fonctionnalité
Conception Les principes de base sont insuffisants Encapsulation, héritage, polymorphisme Les Design Patterns ne forment pas un tout suffisamment cohérent Nécessité de principes plus généraux et de plus haut niveau
Risques de la conception Rigidité Chaque évolution entraîne trop de changements Fragilité Une modification peut générer des erreurs Immobilité Réutilisation difficile
Solution aux risques Gestion des dépendances Évite l'effet «spaghetti» Renforce Robustesse Extensibilité Réutilisabilité
Gestion des évolutions et des dépendances
Principes premiers
Principe d'ouverture/fermeture (1) Open-closed principle (OCP) [ Bertrand Meyer ] Un module doit être ouvert aux extensions Mais doit être fermé aux modifications Package, classe, méthode
Principe d'ouverture/fermeture (2) Ouvert aux extensions Le module peut être étendu pour proposer des comportements qui n'étaient pas prévus lors de sa création Fermé aux modifications Les extensions sont introduites sans modifier le code du module Ajouter du code mais pas éditer du code
Solution: plus d'abstraction Délégation abstraite Utilisation de classes abstraites Utilisation d'interfaces (Java) Attention au principe d'isolation OCP et Design Patterns Strategy, Abstract Factory, Visitor
OCP: exemple 1 fermée aux modifications la méthode correspondre() ne change pas ouverte aux extensions car les sous-classes peuvent modifier le comportement de la méthode correspondre()
OCP: exemple 2 délégation
Principe de non répétition Don't Repeat Yourself Principle (DRY) [ Andy Hunt, Dave Thomas ] Principe connu aussi sous l'acronyme SPOT* Éviter le code dupliqué Solution Passer par une abstraction qui regroupe le code commun * Single Point Of Truth
Principe de responsabilité unique The Single Responsability Principle (SRP) [ Robert Cecil Martin ] Chaque objet n'a qu'une seule responsabilité Ce principe permet lors de modifications de n'avoir qu'une seule raison de modifier une classe d'objets.
SRP: exemple La classe Automobile agrège trop de responsabilités. D'un point de vue conceptuel certaines responsabilités n'ont pas à caractériser cette classe La solution, comme souvent est d'encapsuler ces responsabilités dans des classes solution
Principe de substitution Substitution Principle (LSP) [ Barbara Liskov ] Les méthodes qui utilisent des objets d'une classe doivent pouvoir utiliser des objets dérivés de cette classe sans même le savoir Les classes d'implémentation doivent se conformer aux interfaces Concept du Design by contract [ Bertrand Meyer ]
LSP: exemple 1 public void Dessiner(Forme uneforme) { if (uneforme instanceof Carre) DessinerUnCarre( Carre (uneforme)); else if (uneforme instanceof Cercle) DessinerUnCercle(Cercle(uneforme)); }
LSP: exemple 2 public class Rectangle { private double longueur; private double largeur; public void setLongueur(double arg) { longueur = arg; } public void setLargeur(double arg) { largeur = arg; } public double getLongueur() { return longueur; } public double getLargeur() { return largeur; } } public class Carre extends Rectangle { } public static void main(String arg[ ]) { Carre c = new Carre(); c.setLongueur(5); c.setLargeur(8); }
LSP: exemple 2 suite public class Rectangle { private double longueur; private double largeur; public void setLongueur(double arg) { longueur = arg; } public void setLargeur(double arg) { largeur = arg; } public double getLongueur() { return longueur; } public double getLargeur() { return largeur; } } public class Carre extends Rectangle { public void setLongueur(double arg) { super.setLongueur(arg); super.setLargeur(arg); } public void setLargeur(double arg) { super.setLongueur(arg); super.setLargeur(arg); } public void methodeA(Rectangle r) { r.setLongueur(25); } public void methodeB(Rectangle r) { r.setLongueur(6); r.setLargeur(5); assert r.getLongueur() * r.getLargeur() == 30; }
Autres principes
Principe d'inversion Dependency Inversion Principle (DIT) Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Tous deux doivent dépendre d'abstractions. Les abstractions ne doivent pas dépendre de détails. Les détails doivent dépendre d'abstractions.
ISP: exemple (1)
ISP: exemple (2)
Principe de séparation Interface Segregation principle (ISP) Les clients ne doivent pas être forcés de dépendre d'interfaces qu'ils n'utilisent pas.
Principe d'équivalence Reuse-Release Equivalence Principle (REP) La granularité en termes de réutilisation est le package. Seuls des packages livrés sont susceptibles d'être réutilisés.
Principe de réutilisation Common Reuse Principle (CRP) Réutiliser une classe d'un package, c'est réutiliser le package entier
Principe de fermeture Common Closure Principle (CCP) Les classes impactées par les mêmes changements doivent être placées dans un même package.
Principe des dépendances Acyclic Dependencies Principle (ADP) Les dépendances entre packages doivent former un graphe acyclique.
Principe de relation Stable Dependencies Principle (DP) Un package doit dépendre uniquement de packages plus stables que lui.
Principe de stabilité Stable Abstractions Principle (SAP) Les packages les plus stables doivent être les plus abstraits. Les packages instables doivent être concrets. Le degré d'abstraction d'un package doit correspondre à son degré de stabilité.
Résumé (1) Isoler les parties génériques/réutilisables de l'application en les faisant reposer uniquement sur des classes d'interface. Considérer l'héritage comme une implémentation d'interface, la classe dérivée pouvant se brancher dans n'importe quel code qui utilise cette interface (l'interface forme alors un contrat entre le code utilisateur et les classes dérivées). Utiliser des classes d'interfaces pour créer des pare-feu contre la propagation des changements.
Résumé (2) Construire les parties techniques de l'application sur les parties métier, et non l'inverse. Décomposer l'application en packages pour gérer correctement les versions et permettre une réelle réutilisation. Regrouper dans un même package les classes qui sont utilisées ensemble et qui sont impactées par les mêmes changements.
Résumé (3) Organiser les modules en un arbre de dépendances (en supprimant tout cycle dans le graphe des dépendances). Placer les packages les plus stables à la base de l'abre. Placer les interfaces dans les packages les plus stables.
Bibliographie 1 Design Patterns : Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Addison-Wesley, Pattern-Oriented Software Architecture, Volume 1: A System of Patterns, Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, Michael Stal, John Wiley & Son Ltd, Multi-Paradigm Design for C++,Jim Coplien,Addison-Wesley, 1998.
Bibliographie 2 Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant,William Opdyke,Don Roberts, Addison-Wesley Professional,1999. Agile Software Development: Principles, Patterns, and Practices, Robert Cecil Martin, Prentice-Hall, The Pragmatic Programmer: From Journeyman to Master, Andrew Hunt (Préface), David Thomas (Auteur), Addison-Wesley Professional (octobre 1999)