S.O.L.I.D PAR LA PRATIQUE en JAVA(3è PARTIE)
INTRODUCTION
Dans les précédents articles nous avons parlé d'abord de ce que veut dire l'acronyme S.O.L.I.D et de son importance. Ensuite, nous avons abordé de manière singulière la première lettre de l'acronyme: "S". Je rappelle que notre objectif est de découvrir par la pratique le rôle et l'impacte de chaque notion que revêt chaque lettre de l'acronyme SO.L.I.D.
Pourquoi faire du code selon les principes S.O.L.I.D?
- un code avec pas ou moins de bugs
- un code propre et lisible
- un code cohérent donc logique et compréhensible
- un code facilement maintenable
- un code extensible ou facilement évolutif
Aujourd'hui, nous allons continuer notre marche vers la découverte de la notion de la lettre "O". Pour se faire , je vous propose la démarche suivante:
1- "O": Ouvert à l'extension mais fermé à la modification, qu'est ce que cela représente pour nous les devs?
2- Zoomons sur deux cas concrets afin de s'approprier des bonnes pratiques liées à cette notion.
3- Quelques conseils pour nous aider à œuvrer à mettre en place un code propre, cohérent, compréhensible, facilement testable, maintenable et évolutif.
1- Que comprendre de: "Ouvert à l'extension, fermé à la modification"?
" les entités doivent être ouvertes à l'extension
ET
fermées à la modification"
Mise en situation:
Vous faites partir d'une scrum team et vous travaillé avec l'équipe sur un projet à forte valeur ajoutée. Les règles de gestions de ce projet sont sujettes à évoluer en fonction des objectifs marketing pour s'adapter à l'évolution du marché. Lors d'une sprint review, il ressort qu'une fonction doit être ajouter au sprint qu'on vient de finir car, elle permettrait d'apporter une innovation donc s'adapterait aux nouveaux besoins des utilisateurs.
Comment faire? Sommes-nous sur que l'ajout de cette fonctionnalité ne va pas nous obliger à revoir tout le code des autres fonctionnalités?
Alors si tel est le cas, notre code ne respecte pas le prince: "ouvert à l'extension, fermé à la modification".
En effet, par ce principe, nous devons comprendre qu'il faut qu'a l'ajout d'une quelconque fonctionnalité à une partie de notre code ne doit en AUCUN cas nous emmener à modifier tout notre code afin de prendre en compte cette fonctionnalité. Imaginez-vous en moins de 3 mois, on nous a demandé d'ajouter 3 fonctionnalités. A force de modifier, on finira par écrire un code "spaghettis"(touffus), difficile à comprendre, très compliqué à tester et que dire de sa maintenance?
Beaucoup de devs rencontrent assez fréquemment cette situation. La plupart des devs pensent que ces principes ou ces techniques en générale, sont compliquées donc réservées à une élite: ceux qu'on qualifierait de "génies". Pourtant il n'en n'est rien de tout ça. En début de projet, ils n'intègrent pas ces bonnes pratiques. L'erreur qui est commise c'est qu'ils se disent "c'est pour ce type de produit que j'implémente la fonctionnalité" ou encore " si c'est pour gérer uniquement ceci, on peut faire ça". En fait les devs ont toujours tendance à gérer les fonctionnalités au cas par cas et se retrouvent à "bricoler" le code pour prendre en compte d'autres fonctionnalités.
Peu importe le projet, les bonnes pratiques en la matière doivent être dans l'esprit du dev comme un référentiel. D'abord pour lui: il se retrouvera facilement dans son code, ensuite pour les autres(devs, testeurs) qui vont soit tester son code, soit le comprendre pour le maintenir et le faire évoluer. Enfin, pour l'entreprise afin de lui éviter de perdre en temps et en budget.
NB: ce principe s'applique aussi bien aux entités qu'aux services, modules, fonctions...
2-Cas pratiques
Exemple:1
Vous travaillez sur un projet qui a assez de fonctionnalités intéressantes. Un pan de cette application permet en fait de déterminer si l'utilisateur est majeur ou pas afin de lui afficher un contenu approprié à sa catégorie. La seule chose que vous avez comme base, c'est l'âge que vous recevez comme un élément d'une réponse par appel d'un micro-service.
Voilà qu'un jour, on se rend compte que l'âge de la majorité est différente en fonction du pays où se trouve l'utilisateur. En effet, pour rester plus simple mais en même temps concis, nous allons prendre 3 pays: La Côte d'Ivoire, le Cameroun et la France en considérant respectivement que les âges de la majorité sont de 21 ans, 20 ans et 18 ans. L'idée ici, c'est de faire évoluer notre code afin d'intégrer cette nouvelle contrainte fonctionnelle.
A) Code ne respectant pas le principe de "O"
public class ValidationAgeUtilisateur {
public boolean isMajor(int age){
if(age>=18){
return true;
}
return false;
}
}
Essayons de prendre en compte l'évolution demandée(l'exemple est le plus simple possible).public class ValidationAgeUtilisateur {
public boolean isMajor(int age, String pays){
if(age>=21 && pays == "Côte d'ivoire"){
return true;
}else if(age >= 20 && pays == "Cameroun"){
return true;
}else if(age >= 18 && pays == "France" ){
return true;
}
return false;
}
}La signature a changé ce qui n'est vraiment pas bon pour le reste du code qui fonctionnait bien auparavant.Dans ce cas de figure on pouvait également utiliser "Switch case" en considérant l'âge comme le cas à vérifier,et pour chaque cas avec un "if" on pourra vérifier également le pays de l'utilisateur et faire le traitement approprié.Allez plus loinIl peut arriver que nous soyons dans un cas ou il y'a déjà un minimum de refactoring avec l'existenced'une interface par exemple. De quoi est-ce que je parle?Je parle par exemple d'un code dont une interface gère le comportement des motos normales.Entendez par "normales" les motos qui ne sont pas électriques. Cette gestion fonctionne depuisdes années. Aujourd'hui, avec le réchauffement climatique, tout le monde s'oriente vers l'énergie verte.Ainsi on vous demande en tant que dev, de prendre en compte dans votre programme la productionde motos électroniques et hybrides.La chose la plus simple est d'avoir deux autres interfaces(electricMotor et hybridMotor) qui vont chacunehériter de l'interface existante ce qui permet d'assurer le bon fonctionnement du code existant(sans modification)et de prendre en compte les nouvelles exigences fonctionnelles(l'extension). C'est d'ailleurs cette pratique adoptéeavec JPA quand nous créons notre interface et que nous la faisons hériter de JpaRepository ou CrudRepository.B) Code respectant le principe "O": bonne pratiquepublic interface ValidationAgeUtilisateurParPays {
boolean isMajor(int age);
}public class FranceUtilisateur implements ValidationAgeUtilisateur{
@Override
public boolean isMajor(int age) {
//contrôle
return false;
}
}public class CoteIvoireUtilisateur implements ValidationAgeUtilisateur{
@Override
public boolean isMajor(int age) {
//contrôle
return false;
}
}public class CamerounUtilisateur implements ValidationAgeUtilisateur{
@Override
public boolean isMajor(int age) {
//contrôle
return false;
}
}
Exemple:2
Vous avez développez un module de paiement utilisé par le client pour finaliser son achat en ligne. Sauf que, il y'a encore un mois on vous avez demandé de prendre en compte uniquement un type de cartes bancaires. Aujourd'hui, après une bonne étude diligentée par l'équipe de marketing, un autre moyen de paiement(un autre type de carte) a vu le jour et qui facilite d'avantage le paiement. Et celui-ci commence à faire l'unanimité auprès de tous les clients qui ne jurent que par ce dernier pour leurs opérations. On demande à l'équipe d'ajouter ce nouveau moyen de paiement dans la liste des moyens existants. Celui-ci a une spécificité.
A) Code ne respectant pas le principe de "O"
class CarteBleu {
//attributs et autres }
class MasterCard
{
// attributs et autres }
public class PaiementElectronique {
public double calculePaiement(MasterCard carte){
if(carte instanceof CarteBleu){
// traitement
}if(carte instanceof MasterCard){
// traitement
}else{
return 0;
}
}
Aujourd'hui, votre mission(si vous le voulez bien 😃) est d'ajouter PayPal comme nouveau moyen de paiement. Le réflexe peut être de créer encore une autre class Paypal et venir ensuite dans la méthode de paiement pour contrôler le type de l'instance avant d'appliquer le traitement adéquat. On peut mieux faire.B) Code respectant de principe "O": bonne pratiqueclass CarteElectronique {
//attributs communs et autres
}
class CarteBleu extends CarteElectronique {
//attributs spécifiques et autres }
class MasterCard extends CarteElectronique
{
// attributs spécifiques et autres }
class PayPal extends CarteElectronique
{
// attributs spécifiques et autres }
public class PaiementElectronique {public double calculePaiement(double d){// traitement}
}
public class PayPal extends PaiementElectronique {public double calculePaiement(double d){// traitement}}public class MasterCard extends PaiementElectronique {public double calculePaiement(double d){// traitement}}public class CarteBleu extends PaiementElectronique {public double calculePaiement(double d){// traitement}}
3- Quelques conseils pour adopter cette notion
- Il faut garder à l'esprit que toute fonctionnalités est appelées à évoluer dans le temps: soit en ajoutant d'autres paramètres soit en les réduisant.
- Si vous constatez que vous commencé à utiliser abusivement "instanceOf", "switch case" ou encore assez de "if" pour intégrer les nouvelles fonctionnalités souvent en fonction du type, vous devriez savoir sans doute que vous êtes partie pour produire un code qui n'est pas de qualité.
- Il faut privilégier l'utilisation des interfaces pour y factoriser un certain nombre de comportement dont l'implémentation concrète sera faite par chaque class en fonction du besoin spécifique(c'est souvent très utile et intègre la notion de couplage faible dans votre code).
- Nous avons des méthodes dans la pratique du développement qui nous permettent d'étendre notre code sans le modifier: l'héritage, les design patterns de structure, la surcharge...

Commentaires
Enregistrer un commentaire