Il y a 7 années -

Temps de lecture 8 minutes

Les principes SOLID

A l’heure où de nombreux débats font rages sur l’éventuel successeur du langage Java, le XKE (Xebia Knowledge Exchange) de Mars a été l’occasion de faire un retour sur les fondamentaux de la conception Orienté Objet.

Faisons un petit sondage dans la communauté : quels arguments avanceriez-vous en faveur du langage Java ?

Parmi les principaux arguments, il est fort à parier que les réponses seront en majorité les applets (heu… non plus maintenant… mais souvenez vous dans les années 90), la JVM, le cross-platform, le monde Open Source et la Communauté, les nombreux frameworks, la simplicité d’écriture et la gestion de la mémoire, les IDEs, etc.

Nous nous apercevons que le fait même que Java soit un langage objet est secondaire. Alors pourquoi programme-t-on en Java ou dans un langage objet ?

La conception objet

Au premier abord les concepts objets ne sont pas simples à appréhender. Il est plus facile de raisonner de manière linéaire et concrète, que de manière abstraite et avec des notions d’héritage ou de polymorphisme. Cela nécessite un certain recul et une capacité d’abstraction qui n’est pas totalement intuitive et plus difficilement accessible.

La conception orientée objet offre des mécanismes uniques de représentation de notre environnement mixant données et comportement, déclinant des notions génériques en de multiples plus spécialisées.

Outre cette capacité à représenter les choses, la conception orientée objet offre également des mécanismes de tolérance au changement très puissants. Cette tolérance est un des facteurs clés du développement logiciel, la maintenance demandant souvent des efforts croissants au fil du temps et de l’évolution du logiciel.

Mais comment caractériser l’intolérance au changement ? En voici les symptômes :

  • La rigidité : « Chaque changement cause une cascade de modifications dans les modules dépendants. » Une intervention simple au premier abord se révèle être un véritable cauchemar, à la manière d’une pelote de laine qui n’a pas de fin.
  • La fragilité : « Tendance d’un logiciel à casser en plusieurs endroits à chaque modification. ». La différence avec la rigidité réside dans le fait que les interventions sur le code peuvent avoir des répercussions sur des modules n’ayant aucune relation avec le code modifié. Toute intervention est soumise à un éventuel changement de comportement dans un autre module.
  • L’immobilisme : « Incapacité du logiciel à pouvoir être réutilisé par d’autres projets ou par des parties de lui même. » – « Il est presque impossible de réutiliser des parties intéressantes du logiciel. » L’effort demandé et les risques encourus sont trop importants.
  • La viscosité : « Il est plus facile de faire un contournement plutôt que de respecter la conception qui a été pensée. » Lorsque nous intervenons sur du code existant, nous avons souvent plusieurs possibilités d’implémentations. Nous allons retrouver des solutions préservant, améliorant le design du code et d’autres qui vont « casser » ce design. Lorsque cette dernière catégorie est plus simple à mettre en place, nous considérons que le code est visqueux.
  • L’opacité correspond à la lisibilité et la simplicité de compréhension du code. La situation la plus courante est un code qui n’exprime pas sa fonction première. Ainsi, plus un code est difficile à lire et à comprendre et plus nous allons considérer que ce dernier est opaque.

Nous le verrons dans la prochaine partie, la conception objet, de par ses capacités d’abstraction, va nous permettre de prévenir ces différents symptômes et ainsi faciliter la maintenance de notre système.

Les principes SOLID

Dans le livre Agile Software Development, Pinciples, Patterns and Practices, Robert C. Martin a condensé, en 2002, cinq principes fondamentaux de conception, répondant à cette problématique d’évolutivité, sous l’acronyme SOLID :

  • Single responsibility principle
  • Open close principle
  • Liskov principle
  • Interface segregation principle
  • Dependency inversion principle

Single Responsibilty Principle

« A class should have one reason to change. »

Comme son nom l’indique, ce principe signifie qu’une classe ne doit posséder qu’une et une seule responsabilité. Mais pourquoi me direz-vous ? Si une classe a plus d’une responsabilité, ces dernières se retrouveront liées. Les modifications apportées à une responsabilité impacteront l’autre, augmentant la rigidité et la fragilité du code.

Un certain nombre de techniques peut nous aider à appliquer le principe de SRP. Parmi elles nous retrouvons les CRC cards — Class Responsibility Collaboration.

Open Closed Principle

« Classes, methods should be open for extension, but closed for modifications. »

Une classe, une méthode, un module doit pouvoir être étendu, supporter différentes implémentations (Open for extension) sans pour cela devoir être modifié (closed for modification).

Les instanciations conditionnelles dans un constructeur sont de bons exemples de non respect de ce principe. Une nouvelle implémentation aura pour impact l’ajout d’une condition dans la méthode.

Voyons sur un exemple, la violation de ce principe :

Public Car(EngineTypeEnum engineType) {
    if (engineType == EngineTypeEnum.FUEL) {
        Engine = new FuelEngine() ;
    } else if (…) {
        …
    }
}

Dans cet exemple nous voyons que l’ajout d’un type d’Engine va entraîner une modification du constructeur : nous sommes en violation avec la deuxième partie du principe – Closed for modification.

Mais comment respecter ces deux notions en même temps ?

Plusieurs solutions s’offrent à nous. La première consiste à utiliser l’injection de dépendance dans sa forme la plus simple :

public Car (Engine engine) {
   this.engine = engine ;
}

Il sera préférable d’utiliser une fabrique d’objets ou de l’injection de dépendance pour réduire le couplage.

public Car (EngineType engineType) {
   engine = EngineFactory.getEngine(engineType) ;
}

@Inject
Engine engine

Ces solutions ne sont bien sûr pas exhaustives.

Liskov Substitution Principle

« Subtypes must be substituable for their base types. »

Les sous classes doivent pouvoir être substituées à leur classe de base sans altérer le comportement de ses utilisateurs. Dit autrement, un utilisateur de la classe de base doit pouvoir continuer de fonctionner correctement si une classe dérivée de la classe principale lui est fournie à la place.

Cela signifie, entre autres, qu’il ne faut pas lever d’exception imprévue (comme UnsupportedOperationException par exemple), ou modifier les valeurs des attributs de la classe principale d’une manière inadaptée, ne respectant pas le contrat défini par la méthode.

Les cas de violation du principe de Liskov ne sont pas si fréquents en réalité et nous concevons en général des modèles qui ne violent pas ce principe. Cependant, cela peut se produire par précipitation ou méconnaissance des détails d’implémentation des classes de base et sa détection est, la plupart du temps, difficile. Je vous encourage donc à rester vigilant lorsque vous décider d’étendre une classe.

Pour vous aider à détecter les violations du principe de Liskov, vous pouvez utiliser la conception par contrat (Design By Contract (DBC)). Certains langages, comme Eiffel, propose nativement ce type de conception.

Interface Segregation Principle

« Clients should not be forced to depend on methods that they do not use. »

Le but de ce principe est d’utiliser les interfaces pour définir des contrats, des ensembles de fonctionnalités répondant à un besoin fonctionnel, plutôt que de se contenter d’apporter de l’abstraction à nos classes. Il en découle une réduction du couplage, les clients dépendant uniquement des services qu’ils utilisent.

L’utilisation systématique d’interface de type IMaClasse reprenant les méthodes publiques de la classe MaClasse n’est par conséquent pas une bonne pratique car cela lie nos contrats à leur implémentation, rendant délicat la réutilisation et les refactorings à venir.

Une mise en garde cependant : un des travers de ce principe peut être de multiplier les interfaces. En poussant cette idée à l’extrême, nous pouvons imaginer une interface avec une méthode par client. Bien entendu, l’expérience, le pragmatisme et le bon sens sont nos meilleurs alliés dans ce domaine.

Dependency Inversion Principle

« High level modules should not depend on low level modules. Both should depend on abstractions. »

« Abstractions should not depend on details. Details should depend on abstractions. »

Attardons-nous sur la notion importante de ce principe : Inversion. Le principe de DIP stipule que les modules de haut niveau ne doivent pas dépendre de modules de plus bas niveau. Mais pour quelle raison ? Pour répondre à cette question, prenons la définition à l’envers : les modules de haut niveau dépendent de modules de bas niveau. En règle générale les modules de haut niveau contiennent le cœur – business – des applications. Lorsque ces modules dépendent de modules de plus bas niveau, les modifications effectuées dans les modules « bas niveau » peuvent avoir des répercussions sur les modules « haut niveau » et les « forcer » à appliquer des changements.

A travers cet exemple nous voyons que les modules de haut niveau sont difficilement réutilisables pour de multiples contextes : les modifications d’un contexte donné sont susceptibles d’entraîner des changements dans les autres contextes. Une solution consiste à rendre indépendants les modules de haut et bas niveau.

Pour plus de détails, une courte explication de ces principes est disponible à cette adresse.

SOLID or not SOLID ?

Tolérance au changement et productivité

Nous l’avons vu, la conception objet et le respect de principes simples permettent de rendre un logiciel plus souple, plus évolutif, moins dépendant de son environnement et de ses évolutions. Néanmoins, cela nécessite un niveau d’abstraction élevé qui n’est pas toujours utile. En effet, un autre principe tend à s’opposer à l’application systématique des principes SOLID : le fameux YAGNI (« You Ain’t Gonna Need It »). Pourquoi mettre en place tout un système de tolérance au changement dans un système statique, avec peu de dépendances externes ? Notre logiciel sera effectivement extrêmement robuste envers des événements qui n’auront pas lieu, ou avec un impact minime. L’investissement effectué s’avère peu productif, avec un retour sur investissement négatif.

Une fois de plus, la raison, le pragmatisme et l’expérience vont nous permettre d’arbitrer sur une conception plus ou moins abstraite, plus ou moins tolérante au changement. Un module commun à plusieurs applications sera nécessairement plus abstrait afin de réduire le couplage. Des composants internes à une application ne nécessiteront pas le même effort. Nous n’allons pas rendre toutes les créations d’objet indépendantes des implémentations sous peine d’avoir plus d’interfaces et de factories que de classes concrètes. On peut envisager de commencer une implémentation sans avoir recours à l’ensemble des principes précédents et de les mettre en œuvre quand la nécessité s’en fait sentir (ajout d’un cas particulier, d’une condition, d’une nouvelle implémentation, etc.). L’apparition d’instanciations conditionnelles, de conditions sur le type d’une classe, sont autant de signaux en faveur de SOLID et nécessitent d’engendrer une réflexion sur un éventuel refactoring. Celui-ci fait en effet partie intégrante du développement et il n’est pas choquant d’ajuster les choix de conception au moment où l’on en a besoin. On gagnera ainsi en productivité sans pour autant sacrifier la qualité de l’application.

Un indicateur pour le refactoring

Pour savoir si le code d’une application ne respecte pas les principes SOLID et s’il est nécessaire de les appliquer, il est possible d’utiliser les métriques structurelles d’instabilité (relative au couplage) et d’abstraction pour calculer le rapport entre le degré d’utilisation d’un module et son niveau d’abstraction. Un module sera d’autant plus stable qu’il aura de dépendances entrantes par rapport à ses dépendances sortantes.

Le « Stable Abstraction Principle » part du principe que les packages stables doivent être abstraits.

Un module majoritairement utilisé par d’autres (défini comme stable) devra avoir un niveau d’abstraction plus important. Inversement, un module qui n’est utilisé par aucun autre, n’aura aucun intérêt à être abstrait.

Ces métriques sont des indicateurs et doivent être utilisées comme tels, en prenant en compte le contexte de chaque projet.

Publié par Nicolas Jozwiak

Nicolas est delivery manager disposant de 12 ans d’expérience en conception et développement. Son parcours chez un éditeur avant son entrée chez Xebia lui a notamment permis de développer de solides compétences dans le domaine de la qualité et de l’industrialisation (tests, intégration continue, gestion de configuration, contrôle qualité). Bénéficiant d’une expérience très solide de mise en place des méthodes agiles et d’accompagnement d’équipes sur le terrain, il s’attache à mettre à profit quotidiennement son expérience qui est reconnue pour son approche pragmatique, proactive et pédagogique.

Commentaire

2 réponses pour " Les principes SOLID "

  1. Publié par , Il y a 7 années

    Les principes SOLID, c’est bien si ce n’est le L. Le principe de Liskov est par trop restreint; elle est par exemple incapable d’apporter une solution aux types récursifs.

    Depuis la seconde partie de la décennie 1990, la théorie F-Bound a apporté une réponse en définissant le typage qui est sous jacent à la POO et duquel découle naturellement le polymorphisme: le typage de second ordre pour lequel les types sont des éléments d’une famille polymorphique et liée (appelé classe de types). Le principe, plus général, serait alors du genre : « tout objet d’un type donné peut être remplacé par un objet d’un autre type si les deux types satisfont la même classe de types attendue » ; ce que les rubyistes ont appelés le duke typing (qui est une sorte de typage du second ordre implicite).

    Après, comme implémenter dans un langage le typage de second ordre tel que décrit par la théorie F-Bound de Cook ? Les traits sont un moyen d’exprimer celui-ci de façon explicite en représentant les classes de types.

  2. Publié par , Il y a 7 années

    Pour utiliser les métriques de stabilité de package le plugin Eclipse JDepend, couplé au plugin CodePro Tools pour la résolution des problèmes de liens cycliques entre package, sont très simples et utiles.

    La théorie c’est bien, mais sans bons outils ça n’est pas très productif.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.