Le développement, de STUPID à SOLID

Après plus de 10 ans de développement en entreprise, je reste surpris de la méconnaissance de ces principes SOLID, qui sont pourtant un phare censé nous guider, nous développeurs, dans nos choix techniques. Après réflexion, il s’agit peut-être simplement du fait qu’au premier abord ces principes semblent compliqués et j’admets que la première fois où j’ai lu “Interface segregation principle” je n’étais pas hyper enthousiaste à l’idée d’en apprendre plus. Je vais essayer dans cet article de vous en apprendre un peu plus sur ces principes de la manière la plus simple possible par le biais d’exemples.

Pour la petite histoire

Les principes SOLID ont été introduits par Robert C. Martin (aka Uncle Bob) dans son article « Design Principles and Design Patterns » au début des années 2000, qu’il a par la suite étayé dans son livre “Clean Code” (qui au passage est un must read pour tous les développeurs).

From S.T.U.P.I.D…

STUPID est le penchant inverse de SOLID, mais de quoi parle t-on ? On ne va pas trop s’y attarder, parce que nous ce qu’on veut, c’est écrire du code de qualité, n’est-ce pas ?

  • Singleton: Utilisation de singleton
  • Tight coupling : Couplage fort
  • Untestability : Ne peut être testé
  • Premature Optimization : Des optimisations (trop) prématurées
  • Indescriptive Naming : for(i in myArray), c’est quoi i ? Il contient quoi myArray ?
  • Duplication: le développeur est souvent feignant, pourquoi réécrire la même chose ?

To S.O.L.I.D

Ok, alors qu’est-ce qu’on fait ? Et bien on essaie de suivre au maximum les principes qui suivent

Single responsability principle (SRP)

Une classe ne devrait avoir qu’une seule responsabilité, on devrait pouvoir l’expliquer à n’importe qui en disant “Ce bout de code sert à …”. Si vous commencez à rajouter dans votre phrase “et” ou “ou” c’est que vous êtes entrain de casser ce principe.
Un petit exemple de ce qu’il ne faut pas faire

Image for post

Si vous deviez décrire cette classe à un néophyte, vous diriez quelque chose comme :

“Cette classe permet de récupérer /définir le nom d’un utilisateur et vérifier qu’il ne rentre pas n’importe quoi comme nom”

Le problème est dans le “et”, ce n’est pas la responsabilité de cette classe de vérifier que ce qui est entré par l’utilisateur est valide.

Dans l’idéal nous devrions avoir une classe qui a pour unique responsabilité de vérifier les données entrées par l’utilisateur, puis appelé le setter de notre classe User, si et seulement si, la validation est passée. Ainsi notre classe User se limiterait au code ci-dessous

Image for post

OPEN/CLOSED Principle (OCP)

Que dit ce principe ? Une classe est censée être ouverte à l’extension mais fermée à la modification. En d’autres termes, si vous modifiez le corps d’une méthode, la signature d’une méthode ou le type d’une propriété, vous cassez ce principe.
Je sais que dit comme ça, ce n’est pas très clair alors prenons un exemple concret. Disons que l’on vous a demandé de développer une classe qui permet de vérifier qu’une personne est suffisamment âgée pour conduire une voiture vous feriez sans doute quelque chose comme ça

Image for post

C’est pas mal, chacun a sa responsabilité, mais quel est le problème ici ?
Imaginons que votre entreprise décide de s’exporter aux Etats-Unis. Votre validateur ne fonctionne plus car là-bas ils peuvent commencer à conduire dès 16 ans. Vous allez alors modifier votre classe pour quelque chose comme ça :

Image for post

Bingo, vous avez cassé le principe !
Alors comment on fait ? On peut utiliser des interfaces

Image for post

Liskov substitution principle (LSP)

“ Si B et C sont des implémentations de A, alors B et C doivent pouvoir être inter-changées sans affecter l’execution du programme”

Image for post

Ok, pas de panique voilà un petit exemple de ce qu’il ne faut pas faire

Image for post

Qu’est-ce qui ne va pas, on respecte bien le S, on respecte bien le O ? Et bien on casse le L. Pour reprendre la phrase d’introduction avec notre exemple cela donnerait :

“ Si DbCities et ArrayCities sont des implémentations de CitiesInterface, alors DbCities et ArrayCities doivent pouvoir être inter-changées sans affecter l’execution du programme” mais est-ce le cas ?
Non, on affecte l’execution du programme puisque on est obligé de vérifier quelle implémentation on a utilisé. D’un côté on a un array de villes d’un autre un objet, on ne peut les traiter de la même manière.

Voici un petit exemple de ce qu’on pourrait faire dans ce cas, en changeant uniquement notre classe “DbCities”

Image for post

En rajoutant simplement “toArray”, on s’assure que dans l’execution principale de notre programme, peu importe si l’on récupère les villes depuis la base de données ou depuis un array, in fine nous travaillerons avec un array et n’avons pas besoin d’en vérifier son type.

Interface segregation principle (ISP)

Aucun client ne devrait dépendre de méthodes qu’il n’utilise pas

Prenons à nouveau un petit exemple, votre entreprise possède deux machines à café connectées, une à grains et une à capsules. Ce serait sympa de faire une petite app qui permette de lancer un café depuis son poste (à moins que ça ne fasse rêver que moi ?).

Image for post

Le problème ici c’est que la méthode “grind” ne concerne que la machine à grains, donc nous sommes obligés de vérifier dans l’execution de notre programme s’il s’agit d’une machine à capsules ou à grains.
Ici la solution est de découper les interfaces volumineuses en de plus petites et plus spécifiques

Image for post

Ainsi dans l’execution principale de notre programme, on a simplement à appeler “makeCoffee” sans se soucier s’il s’agit d’une machine à capsules ou à grains.

Dependency inversion principle (DIP)

Les modules de haut niveau ne doivent pas dépendre des
modules de bas niveau

Les deux doivent dépendre d’abstractions

Petite disclaimer avant de commencer, l’inversion de dépendance != injection de dépendances.

Encore un petit exemple, vous développez un site internet et vous devez y intégrer une classe qui va permettre à l’utilisateur de récupérer son mot de passe, en utilisant une base de données MySQL.

Image for post

Pas de problèmes, vous avez un moyen d’accéder à votre base de données MySQL, récupérer le mot de passe (ne faites jamais ça ! les mots de passe doivent être chiffrés et même avec l’accès à la BDD vous n’êtes pas censés récupérer le mdp en clair !) et l’envoyer à l’utilisateur.
Les mois passent, puis finalement la décision est prise dans l’entreprise de passer sur une base de données Postgres. Il va falloir modifier votre classe pour aller récupérer une instance de connexion à postgres plutôt que MySQL.
En y réfléchissant bien, était-ce le rôle de PasswordReminder de savoir quel type de base de données on allait utiliser ? Non, surement pas, son rôle c’est simplement d’envoyer le mot de passe peu importe la BDD utilisée :

Image for post

Ici la solution a été d’isoler les modules en intercalant une interface (abstraction) entre les deux types de connexions. Cela permet de faire le lien entre les modules de bas et haut niveaux. La classe PasswordReminder ne sait désormais plus quel type de connexion il a (postgress/mysql), mais il sait qu’il en a une, qui lui permettra de récupérer le mot de passe de l’utilisateur

Conclusion

Je sais qu’en entreprise il est souvent compliqué de penser à toutes ces choses, il y a des deadlines, des changements imprévus qui remettent en cause toute l’architecture de l’application et une multitude d’évènements qui nous empêchent de prendre le temps de refactorer son code de manière optimale.
Il ne s’agit pas avec ces principes de s’imposer une rigueur sans failles, mais le but est de s’en rapprocher le plus possible, aussi souvent que possible afin de réduire sa dette technique et améliorer la qualité de son code

Une fois ces principes intégrés dans votre cerveau, vous verrez que cela deviendra de plus en plus naturel, votre code sera de meilleur qualité et vous serez par extension un meilleur développeur.

Auteur Yann Levy