Vous êtes sur la page 1sur 10

ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

Principes (SOLID) et Patrons de Conception

INTRODUCTION
Un logiciel peut être confectionné de plusieurs façons différentes. En fait, une même
fonctionnalité peut être codée sous des designs complètement différents et c’est exactement la
source du problème qui fait que les projets de développement ratent souvent leur cible en termes
d’estimation d’efforts. En effet, bien qu’il y ait une architecture prédéterminée, le développeur
est souvent laissé à lui-même lorsque vient le temps d’implémenter la fonctionnalité. Pour faire
une comparaison avec le monde de la construction, c’est un peu comme si on laissait le
menuisier choisir comment monter les murs et arranger les pièces… vous imaginez le bordel
dans une équipe de 10 menuisiers qui travaillent sur une même maison ? Heureusement pour
eux (et pour nous !), ils ont des standards à respecter. Pour notre part, nous avons les patrons
de conceptions et les principes de programmation. On peut donc dire que les design patterns
(patrons de conception en français) naissent via l’expérience. En effet, être confronté plusieurs
fois à un même problème permet d’améliorer et d’affiner à chaque fois la solution qu’on y
apporte. Au bout d’un moment, en fédérant les travaux d’autres personnes ayant été confrontées
au même problème, on va finir par aboutir à une solution qui s’avère être la mieux adaptée pour
ce problème précis. Il suffit ensuite d’en retirer la moelle pour obtenir un modèle réutilisable
permettant de résoudre le problème de manière efficace et performante.
Un design pattern est donc une capitalisation du travail. On peut y voir 3 points positifs :
• Livrer plus vite
• Gérer le changement
• Gérer la complexité
Historiquement, les design patterns proviennent des travaux de Christopher Alexander
(architecte) dans les années 70 et ont été formalisés dans le livre “Design Patterns – Elements
of Reusable Object-Oriented Software” du Gang Of Four en 1995 (ou GoF : groupe de 4
informaticiens, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides, auteurs de
nombreux ouvrages en programmation orientée objet.

Les principes SOLID

L’acronyme SOLID a été inventé par Michael Feathers, auteur du livre Working Effectively
With Legacy Code, mais a été popularisé par Robert C. Martin.
Ces principes de programmation sont la base de tout code qui se veut clair, propre, facilement
maintenable et facile à faire évoluer. Lorsqu’on parle « facilité » de maintenance ou
d’évolution à propos du code, il faut comprendre que cela signifie que le coût nécessaire pour
effectuer un changement à l’application devrait toujours être inférieur aux bénéfices
directement apportés par le changement.

1/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

Généralités.
• Au début de la phase de la conception, le système semble beau, élégant, pur…
• …mais ensuite, pendant le développement, on a des hacks, des implémentations vites
faites et négligentes pour respecter les délais.
• Le logiciel commence à pourrir !
• Quels sont les symptômes d’une conception en train de pourrir ?
• Pour s’assurer de maintenir la qualité de la conception, on doit se baser sur des
principes
• Quels sont ces principes ?
1. SRP (Single Responsibility Principle)
« Parce que tu peux ne signifie pas que tu dois. »
Une classe doit avoir une seule responsabilité.
• Une classe ne devrait avoir qu’une seule responsabilité.
• …mais qu’est-ce que ça veut dire ?
• Une classe ne devrait avoir qu’une seule raison de changer.
• Est-il possible de prévoir les changements d’une classe ?

• Haute cohésion + Faible couplage ➔ Moins de responsabilités, moins de raisons de


changer.
• Alors, maintenez la cohésion et le couplage de la classe !
Le SRP ou principe de la responsabilité unique est probablement le plus connu et le plus
simple à comprendre, puisqu’il est étroitement lié à la cohésion. Le SRP est aux principes
orientés-objet ce que le Singleton est aux patrons de conception. En gros, pour savoir si une
classe respecte le SRP, il faut dire : « La classe X fait … » en étant le plus spécifique possible.
Si la phrase ci-dessus contient des et ou des ou, alors votre classe a plus d’une responsabilité.
De façon plus subtile, si elle contient des mots génériques comme gère ou objet (exemple : gère
les utilisateurs, valide les objets), alors vous devriez avoir la puce à l’oreille.
Par contre, il ne faut pas pousser le concept trop loin. Par exemple, une classe qui formate le
numéro de téléphone d’un utilisateur peut être un peu trop spécifique.

Exemple :
/* Cette classe encapsule les données d’un utilisateur */
public class User {
public String firstName;
public String lastName;
public String phoneNumber;
}

2/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

/* Cette classe encapsule les données d’un utilisateur et valide les informations */

package com.oca.solid.srp;

public class User {


private String firstName;
private String lastName;
private String phoneNumber;

public void setFirstName(String firstName) {


if (firstName == null) throw new Exception("First name is mandatory");
this.firstName = firstName;
}

public void setLastName(String lastName) {


if (lastName == null) throw new Exception("Last name is mandatory");
this.lastName = lastName;
}

public void setPhoneNumber(String phoneNumber) {


if (phoneNumber == null) throw new Exception("Phone number is mandatory");
if (phoneNumber.length() != 10) throw new Exception("Phone number is not
valid");
this.phoneNumber = phoneNumber;
}

// getters...
}

2. OCP (Open/Closed Principle)


« La chirurgie à cœur ouvert n'est pas nécessaire pour mettre un manteau. »

• Les entités logicielles doivent être ouvertes aux extensions, mais fermées aux
modifications.

• Pensez toujours : « qu’est-ce qui va se passer si l’entité change ? »

• L’héritage et le polymorphisme favorisent l’extension.

• Refactoring : Remplacer la vérification de type par du polymorphisme (si la hiérarchie existe


déjà).

• Refactoring : Remplacer la vérification de type par le patron « Stratégie ». (Si la hiérarchie


n’existe pas encore).

L’OCP ou principe ouvert/fermé est beaucoup moins compris que le premier. Lorsqu’on dit
qu’une classe est fermée aux modifications et ouverte à l’extension, il faut comprendre qu’il est
préférable de créer une sous-classe ou d’ajouter des membres pour lui apporter une modification
d’état ou de comportement que de modifier les membres existants.

Donc, si vous devez modifier le corps d’une méthode, sa signature ou encore le type d’une
propriété d’une classe, vous avez de très forte chance d’être en train de briser l’OCP.

3/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

Exemple

Voici une classe qui applique des validations sur l’âge d’un utilisateur :

package com.oca.solid.ocp;

public class UserAgeValidator {


public boolean isOldEnoughToDrinkAlcohol(int age) {
return age >= 18;
}
}

Si nous voulons ajouter de l’information concernant la region et qu’on ne respecte pas l’OCP,
on peut arriver à :
package com.oca.solid.ocp;

public class UserAgeValidator {


public boolean isOldEnoughToDrinkAlcohol(int age, String provinceCode) {
return provinceCode.equalsIgnoreCase('qc') && age >= 18
|| provinceCode.equalsIgnoreCase('on') && age >= 21
}
}
Ce qui est mal avec cet exemple, c’est que nous avons modifié la signature de la fonction,
alors nous aurons assurément des problèmes à la compilation. Au contraire, si on veut
respecter l’OCP, on y aurait été avec une solution plus élégante :

package com.oca.solid.ocp;

public interface UserAgeValidator {


boolean isOldEnoughToDrinkAlcohol(int age);
}

public class LittoralAgeValidator implements UserAgeValidator {


public boolean isOldEnoughToDrinkAlcohol(int age) {
return age >= 18;
}
}

public class CentreAgeValidator implements UserAgeValidator {


public boolean isOldEnoughToDrinkAlcohol(int age) {
return age >= 21;
}
}
3. LSP (Liskov Substitution Principle)
« Si cela ressemble à un canard, sonne comme un canard, mais a besoin de piles, vous avez
probablement la mauvaise abstraction. »
• Si B et C sont des implémentations de A, alors B et C doivent pouvoir être inter-
changées sans affecter l’exécution du programme.
• Toutes les méthodes qui prennent un paramètre de type « Animal », peuvent accepter
un argument de type « Chien ».

4/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

• Les préconditions ne peuvent pas être renforcées dans le type dérivé.


• Les méthodes dérivées ne peuvent pas attendre plus que les méthodes de base.
• Les postconditions ne peuvent pas être plus faibles dans le type dérivé.
• Les méthodes dérivées ne peuvent pas fournir moins que les méthodes de
base.
L’LSP ou principe de substitution de Liskov est très simple, mais son nom fait un peu peur.
En effet, il faut comprendre que les classes partageant une même classe parente partagent aussi
un état et/ou un comportement. Si un comportement est défini dans une classe et qu’une autre
en hérite, alors elle doit se conformer. Si ce comportement n’est pas désiré dans la classe, alors
le LSP n’est pas respecté.
Exemple
public class User {
private String emailAddress;

public String getEmailAddress() {


return this.emailAddress;
}
}

public class AnonymousUser extends User {


public String getEmailAddress() {
throw new RuntimeException("Anonymous users don't have an email address.");
}
}
/*Ici, le fait de lancer une exception à l’appel d’une méthode est un indicatif. Il va de même
pour les méthodes héritées d’une interface ou d’une classe abstraite qui ne ferait que lancer une
exception. En général, c’est un signe que le modèle de données a un problème. Il est vrai qu’un
utilisateur anonyme est un utilisateur, mais il est faux que tous les utilisateurs ont des adresses
courriel. */

5/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

package com.oca.solid.lsp;

public abstract class User {

public class RegisteredUser extends User{


private String emailAddress;

public String getEmailAddress() {


return this.emailAddress;
}
}
public class AnonymousUser extends User {

4. ISP (Interface Segregation Principle)


« Tu veux que je mette ça où ? » leseront
principe consiste à ne pas surcharger de méthode une classe pour qu'une classe n'utilise que les méthodes qui lui
utiles

• De nombreuses interfaces spécifiques aux clients valent mieux qu’une seule


interface.

L’ISP ou principe de ségrégation de l’interface est un autre principe nébuleux que nous
allons démystifier. Le couplage est un principe orienté-objet qui se quantifie en déterminant les
cas d’utilisation des classes. On dit de deux classes qu’elles ont un couplage fort lorsqu’elles
s’utilisent. Afin de diminuer le couplage, il est possible de dépendre d’une abstraction, cachant
ainsi l’implémentation à l’appelant. Le problème avec l’ISP, c’est lorsqu’une interface ou une
classe abstraite devient trop volumineuse et expose trop d’information via son API.

On peut comprendre que l’ISP est lié avec le SRP par le fait que l’interface peut définir plusieurs
concepts qui ne sont pas nécessairement liés.

Exemple

Dans l’exemple ci-dessous, la classe Vehicule a deux comportements. Nous pouvons démarrer
la voiture ou allumer les lumières.

6/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

public class Vehicule {


public void startEngine() {
System.out.println("Engine started");
}
public void turnLightsOn() {
System.out.println("Lights turned on");
}
}

En supposant qu’une deuxième classe aurait la responsabilité d’allumer les lumières comme
suit :

class VehiculeLightTurner
{
public Vehicule vehicule;

public void turnOnTheLights() {


vehicule.turnLightsOn();
}
}

Afin de respecter au maximum l’ISP, il faut se demande pourquoi une classe responsable
d’allumer les lumières devrait savoir qu’un véhicule peut être démarrer. On peut alors extraire
l’interface afin que notre classe ne connaisse que ce dont elle a besoin :
public interface VehiculeWithLights {
void turnLightsOn();
}

public class Vehicule implements VehiculeWithLights {


public void startEngine() {
System.out.println("Engine started");
}
public void turnLightsOn() {
System.out.println("Lights turned on");
}
}

class VehiculeLightTurner {
public VehiculeWithLights vehicle;

public void turnOnTheLights() {


vehicule.turnLightsOn();
}
}

5. DIP (Dependency Inversion Principle)


« Souderiez-vous une lampe directement sur le câblage électrique dans le mur ? »
• Dépend des abstractions. Ne dépend pas de classes concrètes.

7/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

• Si on veut changer la base de données, combien de classes doit-on changer ?


• Si on dépend de bibliothèques logicielles, notre système est enfermé dans un langage
spécifique. (DIP violé)
• Si on dépend des APIs REST, notre système reste indépendant des langages. (DIP
garanti)

Le DIP ou principe d’inversion de dépendance nous dit que les dépendances d’une classe ne
devraient jamais être concrètes. Puisqu’elle ne doit pas connaître l’implémentation de ses
dépendances, nous pouvons nous assurer du respect de ce principe en implémentant le patron
de conception d’injection de dépendances, soit via le constructeur, soit via les mutateurs.

Supposons que vous avez une classe responsable de la journalisation. Cette classe est utilisée
dans un service afin de journaliser les entrées et sorties :

8/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

class Logger {
public void log(String msg) {
System.out.println(msg);
}
}

class SomeService {
private Logger logger;

public SomeService() {
this.logger = new Logger();
}

public void someMethod() {


this.logger.log("Hi!");
}
}
Ici, Logger et SomeService sont fortement couplées. Le problème surgira lorsqu’il faudra
journaliser dans un fichier au lieu de la console, surtout si Logger est utilisée comme telle
partout dans l’application. Par contre, si nous dépendons d’une interface :

class ConsoleLogger implements Logger {


public void log(String msg) {
System.out.println(msg);
}
}

class SomeService {
private Logger logger;

public SomeService() {
this.logger = new ConsoleLogger();
}

public void someMethod() {


this.logger.log("Hi!");
}
}
Malheureusement, nos classes restent encore couplées. Pour sortir l’implémentation
complètement, on doit inverser les dépendances en implémentant l’injection :
interface Logger {
void log(String msg);
}

class ConsoleLogger implements Logger {


public void log(String msg) {
System.out.println(msg);
}
}

9/10
ARCHITECTURE D’APPLICATION CS2I 3 / 3IAC

class SomeService {
private Logger logger;

public SomeService(Logger logger) {


this.logger = logger;
}

public void someMethod() {


this.logger.log("Hi!");
}
}

Et voilà ! SomeService n’a plus connaissance de la technologie utilisée pour la


journalisation.

CONCLUSION
Ces principes, règles et patterns ne sont que des outils permettant de faciliter la rédaction du
code et la création d’applications robustes et évolutives et ils ne sont donc pas toujours à prendre
au pied de la lettre. En fonction de la criticité du module que vous écrivez, il ne sera pas
forcément indispensable de mettre en œuvre une usine à gaz ! Vous pouvez aussi très bien faire
une première version “simple” que vous ferez évoluer au fur et à mesure des besoins (et à ce
moment il sera peut-être nécessaire d’appliquer ces principes).

10/10

Vous aimerez peut-être aussi