Debug d’un process node en prod

TL;DR Executer « kill -usr1 pid », « node inspect -p pid » et le debug mode (en ligne de commande) est à vous.

Récemment, j’ai eu besoin de débugger une app Node.js en production (un setTimeout qui ne fonctionne plus après 25 jours… Original). Seul problème: Comment faire des breakpoints et donc entrer en « mode debug » sur un process qui tourne déjà… Facile!

Commençons avec une app très simple qui affiche « Hello {name} » toutes les 2 secondes.

Pour le lancer en production, nous utilisons la commande « node index.js« .

Time to debug!

Première chose à faire, trouver le process

Ensuite, il faut le passer en mode debug. Pour ce faire, il faut le « killer » avec l’option -usr1 et le pid.

Si tout va bien, vous devriez voir ce message dans vos logs. Comme si vous aviez fait un console.log, donc au même endroit que vos « Hello {name} ».

Vu que mon port 9229 était fermé dans docker, j’ai préféré tout faire en ligne de commande. C’est un peu plus fastidieux, mais ça fonctionne bien!

La prochaine étape est d’écouter/inspecter le process en indiquant le même pid.

Vous devriez avoir un nouveau message dans vos logs:

Nous pouvons, dès à présent, commencer à rentrer dans le vif du sujet. On peut par exemple connaitre la liste des fichiers utilisés:

Ajouter des break points. Pour cela, il suffit d’indiquer le fichier et la ligne.

Attention, car vos fichiers seront légèrement différents que le fichier source que vous connaissez. Voici par exemple le fichier index.js tel que le node l’interprète:

Celui-ci est d’autant plus différent si vous utilisez « ts-node » par exemple. Au début, il faut donc un peu tâtonner pour arriver à la bonne ligne.

Après avoir mis le break point , votre app devrait rapidement arriver à la ligne demandée. Celle-ci se bloque, affiche un petit « > » sur la ligne « en cours », qui attend vos instructions. Les étoiles (*) correspondent à vos autres break points:

A présent, vous avez le choix:

« bt »: Afficher la backtrace:

« c »: Continuer l’exécution (jusqu’au prochain break point si vous en avez d’autres):

« n »: Aller à la prochaine instructions (La prochaine ligne):

« list(n) »: Afficher plus de lignes avant et après la ligne en cours:

« s »: « Entrer » dans la fonction où l’on se trouve (« o » pour en sortir). Vous allez même pouvoir entrer dans du code de node lui-même!

« exec »: Executer/Interpréter du code node « on the fly », dans le context actuel. Pratique pour connaître l’état des variables.

Il existe cependant quelques limitations. Par exemple « require » qui ne peut pas être appelé:

Et voilà, plus d’excuse maintenant! Si vous avez un bug en production, vous avez les outils pour trouver votre bug si vos logs sont incomplets ou si, comme moi, vous avez un problème avec une version de node en particulier 🙂

Le TOKEN API: Simple, JWT et macaron

TL;DR Le macaron, c’est un JWT sous stéroïde…

Si vous créez une application, vous aurez probablement (en tout cas j’espère pour vous) des clients qui vont la consommer.

Si c’est une API, vous allez devoir fournir à vos utilisateurs un token qu’ils devront rajouter dans le header (Authorization: bearer MON_TOKEN). Grâce à cela vous allez pouvoir les identifier mais surtout savoir ce qu’ils ont le droit faire (ou pas).

Mais qu’est-ce qu’on met dans ces tokens? Comment les génère-t-on? Quid de la vérification des droits? Que fais-je? Pourquoi-je?

1. La préhistoire

Dans un cas basique, un token est une bête chaine de caractère plus ou moins longue, liée à un « user id » dans une base de données. Que l’on soit en mode « monolithe » ou « micro service », nous devrons probablement appeler un service d’authentification (user gateway) centralisé pour vérifier la validité du token.

Une fois l’utilisateur authentifié, le backend prend le relais et peut, le cas échéant, refaire des requêtes au « user gateway » pour savoir si l’utilisateur a les droits pour effectuer telle ou telle action. Pour  limiter l’accès au « user gateway », nous allons probablement l’optimiser pour qu’il nous renvoi l’identité de la personne et toute une série de flags correspondant à tous ses droits.

Autant vous le dire tout de suite, notre « user gateway » est un SPOF (single point of failure): S’il crash, c’est terminé! Aucun de nos micro-services ne répondra ou  notre monolithe sera down.

Ça fait également beaucoup des requêtes sur un réseau et qui dit réseau dit: Lent et non-fiable. En mettant du cache, on s’expose à d’autres problème car c’est bien connu, le cache est la chose la plus complexe en informatique…

2. Le JWT? C’est « sooo 2013 »

Ensuite, il y a le Json Web Token (JWT, pour les intimes). Avec lui, ce qui est cool c’est qu’il permet d’embarquer un JSON (appelé payload) intégré dans le token. Ce n’est donc plus une chaine de caractères aléatoires.

Mais pourquoi aurait-on besoin d’un json dans un token?
Je vous donne un exemple: Nous n’aurons plus besoin d’une table de correspondance token<->userId car le userId sera livré dans le payload.

Mais si le userId est dans le JSON, on peut mettre ?
Oui

Mais c’est pas du tout « secure » ton brol?
Mais siii, allez, il y a quand même une sécurité: Tout le monde ne peut pas créer un token!

Comment crée-t-on un token JWT?

La recette est simple! Pour faire un token, nous avons besoin de:

  • Un payload: le JSON.
  • Une clé secrète. Elle peut être symétrique (un secret qui peut être partagé) ou asymétrique (clé publique/privée).
  • Un algorithme de signature. Au choix: HS256, HS512, RS256, RS512, ES256, ES512, etc…

Comme nous pouvons le voir, le format d’un JWT est formée de 3 parties, séparées par un point:

  • La première partie « header », est le « base 64 » d’un json qui indique l’algorithme utilisé
  • La deuxième partie « payload », est le « base 64 » du json que nous voulons envoyer (le userId, etc…)
  • La troisième partie « verify signature », est le « base 64 » de la signature de l’algorithme utilisé.

Comment vérifie-t-on un token JWT ?

Une fois le token envoyé au backend, il faut bien sûr pouvoir le vérifier. Pour ce faire, rien de plus simple, les librairies fournisse la méthode « verify » qui nécessite 3 arguments:

  • Le token
  • Le secret avec lequel le token a été signé
  • L’algorithme qui a permis de signé le token

Grâce à cette méthode, nous pouvons être sûr que le payload est valide car personne ne peut produire un JWT valide à moins de connaître la clé secrète.

Pour récupérer le payload sous format json rien de plus simple:

Le payload n’étant pas crypté, on peut le récupérer avant même d’avoir vérifié la signature du token. Cela peut être pratique dans certains cas. N’y mettez donc jamais de données sensibles (mot de passe, api key, etc…).

Résumé et pistes de réflexion à propos du JWT…

  • Grâce à JWT, on a l’occasion de passer plus d’informations valides grâce au payload.
  • Le payload peut être lu sans connaitre le secret
  • Il faut connaitre un secret pour signer ou vérifier un token… Ce qui implique parfois un secret partagé. A ne pas oublier toutefois: Au moins le secret est partagé, au mieux on se porte.
  • On ne peut pas modifier un token. Pour ce faire, il faut en créer un nouveau sur base d’un autre:
    • Extraire le payload
    • Rajouter/modifier/supprimer des données
    • Créer un autre token avec le même secret et le même algorithme… Il faut donc connaitre le secret.
  • Si le secret est compromis, il faudra en re-générer un autre, ce qui invalidera tous les tokens.
  • Idée: Pourquoi ne pas mettre la « version » du secret pour permettre la migration d’un secret à un autre? Une sorte de dépréciation.
  • Idée: Vu que le secret peut être asymétrique, Il est possible de créer une clé privée/publique par utilisateur. On génère un token avec la clé privée, et on enregistre la clé publique dans la base de données. Cette clé n’est pas critique et peut être volée sans rien compromettre. Ce qui donnerait ceci:
    • Récupération du token dans le header (ou un cookie, une session, etc…)
    • Extraction du champs userId dans le payload
    • Récupération de la clé publique liée à cet utilisateur
    • Vérification du token avec la clé publique
    • Si c’est bon, le token est confirmé et on peut faire confiance aux infos du payload

3. Le macaron, le serial(ized) killer ?

Le macaron est un type de token inventé par des mecs de Google en  2014. Si voulez le papier complet, vous pouvez le télécharger ici: https://ai.google/research/pubs/pub41892.

L’idée assez différente: On va créer un token (toujours signé avec une clé secrète), qui va contenir une liste de conditions (appelés caveats) qui vont permettre de savoir si celui-ci est valide. Un peu comme une liste de « if » super stricte: Le premier qui répond false rend le token invalide. Un caveat est toujours une chaine de caractères, du genre: »time < 2019-01-01″. Dans ce cas-ci, le token est valide jusqu’au 31 décembre.

Comment crée-t-on un macaron?

La recette est simple. Pour faire un macaron, nous avons besoin de:

  • Une location: L’adresse « http » de la ressource que nous voulons accéder. Cette adresse est purement indicative et n’impacte pas la validité du macaron. Il n’y a pas de format spécifique, on peut donc vraiment y mettre ce qu’on veut.
  • Une liste de caveats: conditions sous forme de chaine de caractères. Il n’y a pas de format spécifique.
  • Une clé secrète pour signé le tout
  • Un identifier (ou identifier key): C’est un « indice » sur la clé secrète utilisée pour signer le macaron. Par exemple, vous pouvez mettre « clé numéro 2 » et le backend saura qu’il faudra vérifier le token avec la clé secrète numéro 2. Comme pour la location, cette valeur est purement indicative et n’impacte pas la validité du token. Il n’y pas de format spécifique.

Qu’est ce qui se cache derrière ce gros pavé de base64:

Si on analyse ligne par ligne:

  • location: La location utilisée pour instancier le macaron.
  • identifier: Le fameux « indice » qui indique quel secret on utilise
  • « cid ip = 127.0.0.1 »:  « cid » veut dire « caveat identifier ». C’est une condition qui doit être correcte pour que le macaron soit valide. Si nous avons 2 caveats, il y aura deux lignes commençant par « cid ».
  • « signature »: La signature de l’encryption en base 64.

Comment vérifie-t-on un macaron ?

C’est bien joli tout ça, on a un ou plusieurs caveats qui fonctionnent comme des conditions, mais comment vérifier les conditions? Voilà le code qui vérifie notre macaron précédent:

En fait, ce n’est pas sorcier, la méthode satisfyExact doit recevoir la même chaine de caractères que la méthode addCaveat utilisée pour créer le macaron. Simple, basique! Par défaut, les librairies donne deux méthodes pour vérifier une condition:

  • satisfyExact: Elle ne fonctionne que pour la condition « machin est strictement égal à cette valeur ». Ex: ip = 127.0.0.1.
  • satisfyGeneral: Cette méthode, bien plus puissante, reçoit une fonction/callback en paramètre. Celle-ci sera appelée pour vérifier chaque caveat. Le caveat est d’ailleurs son seul argument. La fonction doit renvoyer « true » si le caveat est correct et « false » dans tous les autres cas.

Pour illustrer l’utilisation de la méthode satisfyGeneral, imaginons ce petit caveat tout simple:

Comme on peut le voir, ce caveat est un peu plus complexe à cause du « < » qui n’est pas implémenté par les librairies. On aurait pu l’écrire autrement (« time is lower than 2019-01-01 » par exemple), mais peu importe, car de toute façon, c’est nous qui allons devoir faire une fonction qui « parse » et vérifie ce caveat. Voici le code de notre « vérifieur »:

Maintenant que notre nouveau « vérifieur de caveat » est prêt, il suffit de  l’ajouter à la liste des « vérifieurs connus » via la méthode satisfyGeneral :

À présent, nous sommes prêts à recevoir un caveat de type « time < … ».

Passe, passe l’macaron!

La deuxième idée du macaron est assez cool: Une fois le macaron en notre possession, il est possible, sans connaitre le secret, de rajouter d’autres caveat. On va donc rendre ce macaron de plus en plus strict car de nouvelles conditions devront être remplies. Il n’est pas possible de modifier ou supprimer un caveat.

Imaginez un peu:

  • Un proxy peur rajouter des caveats sans connaitre le secret partagé.
  • Un utilisateur peut donner son macaron a quelqu’un d’autre en rajoutant un caveat du style « readonly ».
  • Un tier peut rajouter une notion d’expiration sur une ressource.
  • etc…

Moi j’aimerais bien donner des données aussi…

Jusqu’ici, on a vu qu’un macaron, ce n’est qu’une succession de conditions/caveats mais parfois, on aimerait bien pouvoir donner des données comme un JWT. Il y a deux possibilités:

  • Soit on utilise l’ identifier comme gros payload en format json. Vu qu’il n’y a pas de format, ce n’est pas contre indiqué.
  • Après une petite discussion avec Quentin Adam de Clever Cloud, il m’a gentillement montré un bout de code qui permet de faire passer des données via les caveats. L’idée est pas mal du tout: Vu que le format d’un caveat (et son « vérifieur ») peut être custom, nous allons créer un caveat qui dit « enregistre cette valeur dans cette variable lorsque tu vas me vérifier ». Le « vérifieur » va extraire la donnée et renvoyer true.

Voici un exemple:

Et le « vérifieur »:

Et voilà comment on peut s’en sortir pour extraire des données d’un macaron.

Résumé et pistes de réflexion à propos du macaron…

  • Le macaron, comme pour JWT, permet de s’assurer de l’authenticité du token
  • On peut rajouter des caveats sans même connaitre le secret: pratique pour les proxy ou pour partager un macaron avec moins de droits. Cela fait de lui un sérieux outsider car les secrets sont moins éparpillés.
  • Le macaron, comme pour JWT, permet aussi d’extraire des données. Vu que la récupération est custom: The sky is the limit! Peut-être une source d’inspiration?
  • Idée: Pour l’identifier (à la création du macaron), j’ai déjà vu des gens utiliser ce genre du format: « userId:secretType:nonce ». Ce qui permet de:
    • Connaitre facilement le userId.
    • Savoir quel type de secret on a utilisé: une clé publique? un secret partagé? Cela peut aussi changer en fonction des droits de l’utilisateur.
    • Le « nonce » est une valeur incrémentée à chaque requête. Cela permet de vérifier qu’on ne réutilise pas deux fois le même macaron… Pourquoi pas ?

Nous n’en avons pas fini avec les macarons. On reviendra, dans un autre article, sur une autre fonctionnalité plus obscure, mais super puissante: les « third party caveat ». SPOILER: En gros, un autre système vous donne un macaron que vous mixez avec le votre et paf, ça fait (des chocapics) un mix des deux. Vous vous retrouver, dès lors, avec deux autorisations en une.

Selenium webdriver: Le click droit

Voici un mini article pour vous montrer comment faire un click droit dans Selenium webdriver. Contrairement au click « standard », cette action n’est pas prévue de base. Si vous avez affaire à une interface web avec de faux menus contextuels, il faut bien trouver un moyen pour passer outre les limitations du driver!

Heureusement, le javascript permet de simuler le click droit et double-heureusement, Webdriver nous permet d’exécuter du javascript.

Voici une méthode pour « cliquer droit » sur un élément via un sélecteur CSS. Dans l’exemple, jQuery est utilisé pour sélectionner l’élément, mais libre à vous d’utiliser du « pur javascript » s’il n’est pas disponible sur votre site.

Il ne vous reste plus qu’à étendre la nouvelle classe MySelenium2Extended plutôt que PHPUnit_Extensions_Selenium2TestCase pour avoir accès à la méthode.

Voici un exemple de code à l’usage:

Bons tests!

Symfony2: I18N dans les assets

Lorsque vous utilisez une librairie javascript, comme un date picker par exemple, il est bien souvent nécessaire de charger un fichier supplémentaire qui contient les traductions. Fort heureusement, ces fichiers sont nommés avec la « locale » (fr, nl, en, etc…), il est donc assez simple de les charger.

Le problème, dans ce cas-ci, c’est que 2 fichiers sont chargés séparément, alors qu’on favorise plutôt la concaténation.

On pourrait essayer de combiner les fichiers, mais ce n’est pas si simple. Ce code, par exemple, donne l’erreur suivante: Unexpected token « operator » of value « ~ ».

Il existe, cependant, un moyen d’utiliser des variables grâce à ces deux classes (je vous ai surligné les lignes importantes):

Comme vous pouvez le constater, Assetic gère deux variables par défaut: « locale » et « env ». Voyons comment on peut les utiliser…

Configuration

Tout d’abord, il faut les spécifier dans le fichier config.yml, dans la partie twig:

Il est indispensable d’indiquer toutes les valeurs possibles que pourrait avoir la variable au risque de reçevoir une grosse exception! Petit conseil pour la « locale », utilisez un paramètre (%available_locales% par exemple) dans votre parameters.yml afin de ne pas avoir à gérer la liste de vos locales dans plusieurs fichiers différents.

On va ensuite indiquer à Assetic le même tableau de langues et empêcher l’utilisation des controllers pour la génération des assets. Ce deuxième point est malheureusement indispensable.

Utilisation

Le plus dur est fait, vous pouvez maintenant combiner vos javascript classiques et « dynamiques » comme ceci:

  • {locale} sera remplacer par la langue en cours
  • vars est utilisé pour indiquer à Assetic quelles variables il doit gérer dans sa liste d’assets (souvenez-vous, il peut aussi gérer « env »)

Génération des fichiers

On a presque fini! Vu que, pour certains fichiers, on utilise le chemin direct (« bundles/acme/foo » au lieu de  « @AcmeFooBundle »), il faut lancer l’installation de vos assets dans le dossier « /web »:

Il ne reste plus qu’à générer les fichiers « à la main ». Cette commande va parcourir tous vos twigs et générer toutes les combinaisons possibles. Dans notre cas, 3 fichiers seront créés et contiendront chacun le datepicker avec la traduction.

En dev, il est préférable d’utiliser un –watch si vous modifiez régulièrement un de ces fichiers.

Customisation

Si « locale » et « env » ne vous suffisent pas, vous pouvez écrire vous-même votre classe qui gérera d’autres variables. Pour cela rien de plus simple:

  • Créez une classe qui implémente l’interface « Assetic\ValueSupplierInterface ».
  • Changer la classe de la clé « assetic.value_supplier.class » par la votre

Et voilà le travail!

Le SEO: Travail de fin d’études

Suite à une année de cours du soir en marketing (ce qui explique le faible nombre d’articles), j’ai décidé de choisir le SEO (avec une préférence pour Google) comme thème pour mon travail de fin d’année.

Il ne fallait « malheureusement » pas dépasser 40 pages, il n’est donc pas aussi complet qu’espéré. Cependant, je pense qu’il reprend une grosse partie de ce qu’il faut savoir sur les pratiques à appliquer (ou non) pour un bon référencement.

Sans aucune prétention, je met donc mon travail de fin d’études sur le SEO à disposition pour ceux que ça intéresse.

Bonne lecture!

 

Solid – D : Dependency inversion

Voici venu le temps des rires, des chants et du dernier article concernant les principes « SOLID », j’ai nommé: la « dependency inversion» ou, pour les anglophobes, l’inversion de dépendance.

À ne pas confondre avec l’injection de dépendance, même si on n’en est « pas loin », nous verrons pourquoi. Pour comprendre le principe voyons d’abord ce qu’est réellement une dépendance.

Team spirit!

 

Prenons comme exemple deux équipes de programmeurs qui travaillent sur le même projet.

  • L’équipe A s’occupe de créer une classe SmtpMailer,  qui va envoyer un e-mail via SMTP. Une classe d’assez bas niveau donc.
  • L’équipe B s’occupe de créer une classe PromoSender qui va récupérer la dernière promotion et va l’envoyer par e-mail via SmtpMailer.

Pour faciliter la lecture, je vais simplifier le code au maximum.

La classe de la deuxième équipe utilise la classe « Mailer » et donc en dépend. En effet, si un jour on remplace la classe « SmtpMailer », il y a fort à parier que le code de la seconde équipe devra aussi changer.

Voici le code à l’utilisation:

Le lecteur averti que vous êtes, remarquera que nous avons utilisé le principe d’injection de dépendance pour « donner » SmtpMailer à PromoSender. Nous parlons bien d’injection et pas encore d’inversion.

En Uml, cela donnerait ceci:

UserLog uses User

Remarquez la direction de la flèche qui va vers SmtpMailer. La classe PromoSender ne peut pas vivre sans cette dernière car elle l’utilise (ou en dépend si vous préférez). Donc si vous voulez envoyer un e-mail avec une autre classe que SmtpMailer, vous êtes cuit! A moins que…

Première étape: l’abstraction

 

Tout d’abord, voyons ces 2 classes comme faisant partie de 2 packages différents. Je rajoute les couleurs au diagramme pour vous simplifier la vie.

PromoSender uses SmtpMailer

En vert: Equipe A
En orange: Equipe B

Pour faire une « inversion » (et accessoirement bien faire les choses), nous utilisons une interface. L’équipe B se charge simplement d’extraire les méthodes publiques dans une interface qu’ils nomment MailerInterface. Le Sender va alors utiliser l’interface MailerInterface plutôt que la classe concrète: C’est une bonne chose!

En vert: Equipe A
En orange: Equipe B

Notez une chose intéressante à propos de la dépendance par rapport à SmtpMailer: Nous l’avons inversé (Le mot est lâché) ! À présent une flèche part de (et non plus vers) SmtpMailer. Les deux classes se rejoignent sur MailerInterface.

Dans le code, vous vous doutez bien que c’est tout simple. L’équipe A crée l’interface, et l’implémente:

Et pour l’équipe B, il suffit d’indiquer l’interface plutôt que la classe concrète dans le typage de la méthode:

L’essentiel est de ne pas dépendre de l’implémentation, mais d’une abstraction. Grâce à cela, l’implémentation (la classe SmtpMailer) peut être remplacée si votre projet évolue. Libre à vous de créer des ImapMailerGmailMailer, SimulatorMailer etc… tant que ceux-ci implémentent l’interface MailerInterface. Au final, peu importe votre type d’envoi, le code ne bougera pas:

C’est réutilisable et en plus ca répond au principe de l’O – Open/closed!

Voilà LA grande étape de l’inversion de dépendance. Pour résumer grossièrement: il faut utiliser des interfaces. Cependant, il y a deux choses qui ne semblent pas nettes.

  • Tout d’abord, lors du chapitre I – Interface Segregation, nous avons vu qu’une méthode/classe ne devait recevoir que ce dont elle avait besoin, ni plus ni moins. Or, PromoSender n’utilise pas le quart de ce que propose MailerInterface.
  • Ensuite, pour bien faire, les 2 modules devraient pouvoir être utilisables séparément. Si c’est bien le cas pour le package du « Mailer », c’est nettement moins vrai pour PromoSender qui reste dépendant de l’interface d’un autre package.

Voici peut-être venu le moment idéal pour lire la définition exacte du principe d’inversion de dépendance. Que dit Bob Martin (aka Uncle Bob) à ce sujet?

Bob a dit…

 

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.

 

En français, cela donne:

 

Les modules de haut niveau ne devraient pas dépendre des modules de bas niveau. Les 2 devraient dépendre d’abstraction.
Les abstractions ne devraient pas dépendre de détails (=de l’implémentation). Les détails (=implémentations) devraient dépendre d’abstractions.

 

En gros, Bob nous dit qu’il va falloir revoir notre copie. Et ce, pour deux raisons:

  • Le module de haut niveau (PromoSender) dépend directement d’un module de bas niveau (MailerInterface).
  • PromoSender n’a pas sa propre abstraction. Elle utilise une interface d’un autre module et est donc dépendante de l’ « implémentation » de MailerInterface. En effet, si les méthodes de cette dernière changent, PromoSender doit changer également. On a vu mieux au niveau du « couplage ».

Chacun chez soi et les hippopotames seront bien gardés

 

Comment fait-on pour régler ces problèmes? Commençons par le plus simple: Demandons à l’équipe B (aka l’équipe des losers sans interface), de bien vouloir créer une interface que va utiliser PromoSender dans son constructeur. Cela afin de ne plus être dépendant de MailerInterface. On en profitera pour faire des méthodes qui nous « arrangent ».

Et en UML, cela donne:

Les 2 modules sont maintenant indépendants, les interfaces sont bien définies (et différentes), les tests unitaires sont probablement encore plus simples (on ne mock que le nécessaire, comme on a dit lors du I-Interface Segregation)… Mais une chose saute aux yeux: ils sont tellement indépendants qu’ils ne savent plus communiquer ensemble! Nous avons, comme qui dirait, un problème 🙂

Il existe une solution pour faire communiquer 2 modules qui « ne parlent pas la même langue ». Cette solution, nous l’avons déjà vue lors d’un précédent article, c’est bien sûr, le pattern Adapter. Encore lui! Décidément, il est partout. On le voit  d’ailleurs de plus en plus. Surtout dans des projets comme Symfony, Laravel où tout (ou presque) est modulable, externalisable voire même réutilisable en standalone (twig, swiftmailer, oAuth, etc…).

Le but du jeu est de pouvoir utiliser un objet « MyAdapter » comme s’il était un PromoSenderTransportInterface, et qu’il envoie un mail via le MailerInterface choisi.

Ça peut paraitre compliqué, mais ça ne l’est pas. Voici son code où il fait vraiment office de passerelle entre les 2 modules en implémentant une interface et en utilisant l’autre.

A l’utilisation, voici ce que ça donne avec une comparaison avant/après:

 

Voilà à quoi cela va ressembler en UML. Vous remarquerez que les flèches partent de MyAdapter, les 2 modules ne se connaissent toujours pas:

 

 

Comme vous pouvez le voir, l’adaptateur ne fait partie d’aucun des 2 modules. Cependant, il faut bien le mettre quelque part, mais où?

  • Au mieux, il sera créé dans un package à part: celui de votre projet.
  • Au pire, s’il faut vraiment choisir un des deux modules, il pourra être implémenté dans le package où se trouve PromoSenderInterface. En effet, celui-ci est de plus haut niveau, ce qui lui permet de plus facilement « anticiper » ce qu’il va utiliser.
  • Par contre, MailerInterface est de très bas niveau, et ne peut pas anticiper tous les autres modules qui vont l’utiliser.

L’heure du bilan

Aujourd’hui, nous avons vu qu’il était important de bien séparer l’abstraction et l’implémentation.

Tout d’abord, parce que l’abstraction a beaucoup moins susceptible d’être modifiée que l’implémentation. Le code change beaucoup, que ce soit en « change » ou en « bug », mais les interfaces changent très peu.

Ensuite, parce cela vous donne une certaine flexibilité quant à l’objet que vous pourrez donner à vos méthodes. Si vous n’utilisez pas d’interface, vous basculerez vite dans l’héritage « insensé » du style ImapMailer qui étend SmtpMailer qui lui même étend GmailMailer. Sur ce point de vue là, c’est Liskov qui ne sera pas contente.

Séparer abstraction et implémentation, oui, mais pas seulement. En effet, nous avons vu que nous pouvions aussi séparer le code en 2 packages complètement indépendants (ce qui nous rappelle le premier fondement SOLID: S: Single Responsibility). Deux packages différents, rime avec ré-utilisabilité, testabilité, maintenabilité, et plein d’autres choses qui terminent pas « ité », mais aussi plus de cohésion, moins de couplage et donc probablement moins de bugs. On sait également qu’au moins il y a de code, au moins vous rencontrerez de problèmes.

Nous avons vu, encore une fois, la magie de l’adaptateur. Celui-ci n’a pas que des avantages, car il peut aussi complexifier les choses, mais il a le mérite de coller des morceaux entre eux. Certes, si une de nos interfaces venait à être modifiée, il faudra changer l’adaptateur ainsi que ses 2-3 tests unitaires, soit! Mais c’est tout de même mieux que de revoir tout le code de votre package, et tous les malheureux projets qui l’utilisent. Sans parler des tests unitaires qui ne passent plus car plus aucun de vos « mocks » ne sont corrects. Ça fait beaucoup de boulot, vous ne trouvez pas?

Même si cet article n’est pas là pour faire l’apologie de l’adaptateur, sachez qu’il est tout autour de vous, mais qu’il ne s’appelle pas forcément Adapter partout. Voici un simple exemple de comment font les gens pour utiliser Smarty à la place de Twig dans Symfony: via EngineInterface. Vous verrez que la manière de procéder est exactement la même: On crée une classe qui étend une interface (EngineInterface) qui va utiliser un objet d’un autre module (Smarty ), et le tour est joué! Bien évidemment, Smarty et Symfony continuent d’évoluer indépendamment.

SOLID: Fin

Voilà pour le dernier article de SOLID. J’espère que tout cela vous a plu.

Comme vous avez pu le constater, les principes se chevauchent un peu les uns les autres. Au final, en suivant ces quelques règles, il est difficile de dévier de la trajectoire de « bon développeur ».

Cependant, comme dit Ribery: « SOLID, c’est une boite à outils qu’elle est bien de connaitre et de se rappeler ». Il ne s’agit donc pas là d’une vérité absolue. Je ne pense pas qu’un projet puisse être SOLID à 100%. Il ne faut pas tomber dans l’extrême où il existe une interface pour chaque signature de méthode. De même que chaque classe ne doit pas forcément avoir son interface. A l’époque, j’étais tombé sur ce ticket à propos de PimpleFabien Potencier disait:

Using an interface does not really make sense to me as I don’t see what kind of other implementation you might have.

Ça se tient! Quand savoir s’il faut une interface ou pas? Sans doute une question de feeling et d’expérience. L’exemple de l’ O: Open/Closed l’a bien montré: Le code, ça peut aussi se refactorer. Qui ne l’a jamais fait? Cela fait aussi partie de notre quotidien, et c’est comme ça que l’on apprend. C’est beau…

SOLID – I: Interface Segregation

Pour comprendre l’ Interface Segregation. Commençons par la définition de « ségrégation » trouvée dans le Larousse :

« Action de mettre à part quelqu’un, un groupe »

Ou si l’on regarde la définition d’un point de vue technique:

 « Séparation en amas distincts d’un ensemble de corps différents préalablement mélangés. »

Vous adaptez ceci à la programmation et plus spécifiquement aux interfaces, et je vous le donne dans le mile, la ségrégation c’est donc splitter une Interface en 2 (ou plus) si celle-ci prévoit trop de choses. Comment pourrait-on faire trop de choses si l’on suit le premier principe de SOLID ? Suivez le guide!

Pour comprendre ce principe, commençons par un classe native de php: ArrayObject. Lorsque vous étendez cette classe, vous pouvez déjà utiliser votre instance de plein de manière différentes: faire un count, un foreach, le sérialiser et accéder à ses attributs comme un tableau. Jusque là, rien de choquant. Pourtant lorsqu’on y regarde de plus près, cette classe étend quatre interfaces:  IteratorAggregate, ArrayAccess, Serializable et Countable. Que diriez vous de regrouper celles-ci pour n’en faire plus qu’une: ArrayObjectInterface ? Bof… Quel est l’intérêt? Elle ferait sans doute « trop de choses »,  même si, au final, elle n’a qu’une responsabilité: Gérer un objet comme tableau.

Dans ce cas-ci, nous avons fait le « chemin inverse » d’une ségrégation d’interface. Votre job consistera donc à couper une interface en petits morceaux. Un peu comme si monsieur PHP était parti de l’ ArrayObjectInterface pour arriver à IteratorAggregate , ArrayAccess , Serializable et Countable. L’avantage est, bien entendu, de n’étendre que le strict nécessaire. La ségrégation la plus extrême est de ne mettre qu’une seule méthode dans une interface, comme c’est le cas pour Countable par exemple.

 

Mais pourquoi fait-on cela ?

Prenons une interface Contact qui représente une personne qui a plusieurs informations basiques: Nom, prénom, e-mail, téléphone et adresse.

Ajoutons à cela une classe PubSender qui s’occupera d’envoyer des publicités (ou spams) à ces pauvres gens. Une méthode pour l’envoi par sms, l’autre pour les e-mails.

Pour savoir si nous sommes dans le bon, posons nous une première question: Lorsque j’envoie un sms, ai-je vraiment besoin de toutes les méthodes de l’objet Contact ? Ai-je besoin que « mon contact » possède un e-mail, une adresse et un nom de famille?

Biensûr que non! Pour l’envoi d’un sms, vous n’avez besoin que d’un numéro de téléphone et d’un prénom. Le problème est le même pour l’envoi d’e-mail qui n’a besoin que d’un e-mail pour vivre.

Ces méthodes ont donc trop de responsabilités car elle reçoivent de trop gros objets. Il est peut-être temps de diviser notre interface Contact même si elle parait déjà si petite.

 

Ségrégons !

Dans ce cas-ci,  l’idée est de faire en sorte que lorsque l’on envoie un sms, on ne demande qu’un objet qui ne contient que les méthodes que l’on va utiliser. Qu’à cela tienne, nous allons créer une interface Smsable qui représente une personne contactable par sms:

Faisons de même pour les gens qui sont contactables par e-mail.

Mon utilisateur (ou contact) peut donc étendre ces 2 interfaces, et y ajouter d’autres méthodes nécessaires.

Refactorons dès lors notre classe PubSender.

Comme vous pouvez le constater, seule la signature de la méthode change. Nous avons à présent des méthodes qui reçoivent des objets taillés sur-mesure pour la fonctionnalité.

Une chose intéressante avec la ségrégation est que vous pouvez plus facilement réutiliser (décidément LE maitre mot des principes SOLID) les classes. En effet, si les arguments des méthodes sont des interfaces simples, il sera beaucoup plus facile de les intégrer dans un autre projet. C’est aussi moins couteux d’implémenter les 2 méthodes de Smsable plutôt que les 5 méthodes de Contact. Ceci est d’autant plus vrai si 3 des 5 méthodes doivent rester vides car vous ne gérez pas les noms, prénoms et adresses postales. Souvenez-vous de ce que disait Barbara Liskov!

 

Autres avantages

En plus d’avoir des méthodes mieux définies, la ségrégation d’interface apporte aussi un autre avantage : Des mock plus simple pour les tests unitaires.

Fini les grosses classes dont il ne faut mocker qu’une petite partie des méthodes. Mais d’ailleurs, quelle partie ? Quelles méthodes ? Qui n’a jamais été vérifier ce qui est réellement appelé pour savoir ce qui est utilisé (et donc ce qu’il faut mocker)? Cela se passe bien moins souvent lorsque les interfaces sont plus petites, car tout, ou presque, est utilisé.

Utiliser la ségrégation d’interface peut aussi aider à mieux diviser son code (et donc à atteindre/garder la responsabilité unique). En effet, une grosse interface « fourre-tout » est bien plus difficile à maintenir que plusieurs petites qui ont plusieurs avantages:

  • Moins de tests unitaires par classe. En plus d’être petites, on peut penser qu’il y aura aussi moins de méthodes « interconnectées » entre elles.
  • Moins de problèmes de régression car moins de méthodes donc moins de code impacté.

Tout cela donne envie n’est-ce pas?

 

Et si j’envoie un mail ET un sms ?

La partie simple était d’envoyer un e-mail ou un sms. Mais que faire si vous devez envoyer une publicité à votre User sur les 2 supports simultanément (donc via une seule méthode).

Une première solution serait d’abord de faire une nouvelle interface Contactable regroupant les deux interfaces et qui sera implémentée par User.

L’envoi des publicités se fera alors en utilisant cette nouvelle interface

Cette solution est viable. Son principal défaut est que vous vous trouvez avec une nouvelle interface qui ne fait rien d’autre que représenter un ensemble d’autres interfaces. Croisons les doigts pour qu’elle ne grossisse pas trop, sinon vous devrez probablement la ségréguer à nouveau.

Une deuxième solution serait que votre classe User implémente les interfaces de base (Smsable et Emailable) sans passer par une interface intermédiaire. Rien d’extraordinaire pour l’instant.

En écrivant la méthode « send », vous vous dites logiquement que vous aurez besoin de 2 objets, car il y a deux supports: un objet Smsable et un Emailable.

Vous l’avez certainement deviné, comme User étend ces 2 interfaces, on va devoir le passer deux fois à la méthode.

Horreur et stupéfaction! Avant de vous précipiter sur la première solution, mettons les choses au clair.

MultiPubSender veut envoyer une publicité à deux supports différents. Elle n’a clairement pas besoin de savoir si ceux-ci appartiennent à la même personne. Nous n’avons donc aucune raison de changer sa signature, n’est-ce pas? Pourquoi devrait-elle s’adapter à votre objet User?

Oui, le monde est cruel car la même instance de User peut être vu de deux manières différentes (Smsable et Emailable). Mais c’est le problème de User (et le vôtre au passage) pas celui de MultiPubSender! L’inconvénient de cette solution est donc évident: On passe plusieurs fois le même objet à une méthode. Comme on dit chez nous: « ça l’fait pas! ». Par contre, vous laissez une porte ouverte pour le jour où les personnes de contact seront différentes (Open/Closed).

 

Conclusion

La ségrégation d’interface est un chouette principe, si on ne tombe pas dans l’extrême. Si vous poussez le bouchon un peu trop loin, vous n’aurez que des interfaces à une méthode. C’est avec la pratique que vous allez apprendre à bien les utiliser. N’hésitez pas à tester vos connaissances, à vous tromper, refactorer (<> refacturer), re-essayer, etc. Débattez avec vos collègues sur les interfaces à créer ou modifier.

Au début, cela peut sembler compliqué voire inutile pour ceux qui n’utilisent jamais d’interface, mais comme je l’ai déjà dit, elle pourra aussi vous aider à garder un code qui suit les principes de responsabilité unique. Comme quoi, tous les principes SOLID sont liés.

Quant au dernier exemple, un peu tordu mais bien réel, il n’est pas si fréquent que cela. Je suis sûr que chacun trouvera le compromis qui lui convient le mieux : Que ce soit avec une des solutions évoquées ou avec du code supplémentaire comme des adaptateurs, convertisseurs, etc.

Des perles de code pour terminer l’année !

Hello les méchants!

Petite pause de début/fin d’année pour tous. On reprendra les articles sur SOLID début janvier. On parlera également de designs pattern, TDD et aussi d’intégration continue.

Mais en attendant les « vacances », voici 3 « petites » perles (+1 gratuite) à déguster avec sagesse. Au menu:

  • De l’eval comme on les aime en amuse bouche
  • Un commentaire qui fait froid dans le dos, comme entrée
  • Un condition typique des juniors (désolé) comme plat
  • Et pour le dessert, une manière bien spéciale de s’assurer qu’un booléen est un booléen… Enfin je crois!

Trêve de bavardages inutiles et faisons place aux gurus!

 

1. Eval false

On commence léger avec un petit amuse-bouche en javascript. Je ne comprend toujours pas ce que vient faire l’eval ici:

 

2. Le commentaire de la mort

Âmes sensibles s’abstenir… Je rappelle que tout ici est authentique, c’est difficile à croire 🙂

Le développeur ne connait pas les cas qui ne sont pas gérer. Ça nous fais une bonne jambe! Sans test unitaire, cela rend la tache encore plus difficile. Bref, pour ces cas là: vive le TDD

 

3. La condition du noob

Je pense bien faire, je « déclare » ma variable, l’intention y est…

On aurait peut-être pu faire plus simple:

 

 4. Bang bang, you shoot me down

Celle-ci aussi vaut son pesant de cacahuète.

Je ne sais pas pourquoi ce « double bang » au début. Peut-être pour simplifier le fait de « vite changer pour voir si ça fonctionne pas dans l’autre cas »… J’espère au moins qu’il n’a pas fait ça pour s’assurer qu’on ai bien un booléen… Je doute, je ne sais pas, je ne sais plus, je suis perdu.

 

Bonne année!

J’espère que tout cela ne vous a pas donné une indigestion. Je vous souhaite à tous une bonne et méchante année 2014.

A très bientôt!

SOLID – L: Liskov substitution

« Un carré est un rectangle ». Voilà ce que les gentils professeurs de géométrie nous ont dit. Mais, peut-on en dire de même en orienté objet ?

Carre extend Rectangle ?

En orienté objet, un rectangle est constitué de 2 attributs : « largeur » et « hauteur » avec leurs « setter » et « getter ». Rien d’incroyable:

Un carré, par contre, n’a qu’un attribut : « coté », avec un setter et un getter. Mais imaginons qu’il étende la classe Rectangle, comme dans « la vraie vie ». Comment l’objet Carre va-t-il gérer les méthodes setter et getter de rectangle ? Les méthodes « set/getHauteur » et « set/getLargeur » ne sont pas relevantes ou pertinentes pour un carré ! Essayons de contourner le problème:

Tout cela « fonctionne », mais avouez qu’on est plus dans de la bidouille pour faire rentrer un carré dans un rectangle. Ceci est une violation du principe de substitution de Liskov. Notre amie Barbara Liskov (oui c’est une dame) a un jour dit :

Si « S » est un sous-type de « T », alors tout objet de type « T » peut être remplacé par un objet de type « S » sans altérer les propriétés désirables du programme concerné.

Pour en revenir à nos carrés et rectangles, cela veut dire que partout où, dans votre code, vous vous attendez à recevoir un rectangle en paramètre, vous pouvez le remplacer par un carré, sans que cela pose un souci.

Ce n’est malheureusement pas le cas, car le développeur qui travaille sur un rectangle veut pouvoir changer la largeur et la hauteur indépendamment l’un de l’autre. C’est impossible avec un carré, ce qui « altère les propriétés d’un programme ».

Exemple concret

Imaginons que vous fassiez un jeu de voitures. Vous avez une classe « Voiture » avec quelques méthodes :

Votre jeu fait un carton et suite à la pression médiatique, votre patron cède, et vous demande d’ajouter des voitures volantes. « VoitureVolante extend Voiture », ça vous parait pas mal:

Sauf que vous avez la méthode « changePneu » qui vous ennuie un peu, car une voiture volante n’a pas de pneus. Que va faire votre méthode dans ce cas-là? Que pouvez-vous faire ? Au choix :

  • Réécrire la méthode et ne rien faire (méthode vide)
  • Réécrire la méthode et lancer une exception « PasDePneuException ».
  • Réécrire la méthode et nettoyer les réacteurs ou changer le train d’atterrissage. Mais est-ce le bon nom de méthode ?

Aucune des trois réponses n’est viable. En effet, cette méthode n’a pas lieu d’exister dans le cas d’un voiture qui n’a pas de pneu. Conclusion, une voiture volante n’est pas une voiture.

Ça sent le problème…

Voici donc quelques pistes qui vous feront sentir que vous êtes sur la mauvaise voie :

  • Vous avez une méthode qui n’a pas lieu d’être dans votre classe, mais vous devez l’implémenter.
  • Vous avez du réécrire une méthode de la classe parente car celle-ci ne doit « surtout pas » être exécutée ou ne rien faire.
  • Vous avez réécrit une méthode de votre parent et vous lancez une exception, alors que celle-ci n’était pas prévue dans la classe de base. Du coup, vous changez tous les appels en les englobant par un « try/catch »
  • Vous utilisez des « instanceof » pour exécuter (ou non) une méthode spécifique (aussi mauvais que le try/catch)
  • Vous devez modifier une classe parente pour que votre nouvelle classe fonctionne. Par exemple, ajouter des arguments optionnels.

C’est liste est non-exhaustive, mais assez complète. Evidemment, il faut rester vigilant : Certaines de ces affirmations ne sont pas toujours une preuve qu’il y a un problème. Par exemple, les méthodes vides sont monnaies courantes lorsque nous utilisons le pattern « Template method » ou « Null object ». Nous parlerons de ces patterns (et des autres) dans un autre article.

Comment faire rouler ma voiture volante ? Utiliser le pattern « Adapteur »

Bon alors évidemment, c’est difficile de dire à votre patron : « Boss ! Vous savez, dans notre jeu, la voiture bonus qui vole… Hum… Je ne peux pas l’implémenter car elle ne répondra pas au principe de substitution de Liskov ». S’il vous répond qu’il comprend et qu’il ne faut pas l’implémenter, c’est que vous avez vraiment un patron très cool!

Pour ce genre de problème, une des solutions est d’utiliser un pattern : L’adaptateur. Je vais vous en parler très brièvement, car je compte aussi en écrire un article. En gros, c’est faire rentrer un carré dans un rond (ou un rectangle!), ou bien transformer le port « Lightning » de votre IPhone en USB. Voici un petit bout de code qui devrait vous faire comprendre le principe.

D’abord nous avons la nouvelle interface IVoiture et la classe VoitureVolante qui n’ont pas ou peu de point commun:

Ensuite, on va rendre la voiture volante compatible avec la voiture de base grâce à l’adaptateur:

Et à l’exécution, c’est assez simple:

Certes, nous avons créé 2 classes « au lieu » d’une, mais la classe VoitureVolante ne se retrouve pas avec des méthodes poubelles, et ne fait que ce qu’elle doit faire.

Conclusion

Alors, qu’est-ce qu’il en ressort ? Sachez juste que l’orienté objet, ce n’est pas toujours « comme dans la vraie vie ». N’étendez pas une classe qui contient trop de méthodes pour votre nouvelle classe. Le but est de se dire : Si je n’étendais pas cette classe (ou l’interface), est-ce que j’aurais de toute façon écrit cette méthode ? S’appellerait-elle de la même manière ? Aurait-elle le même comportement globale? Est-ce que ma sortie (return) et mes erreurs (throw new Exception) sont identiques à ma classe parente?

Si vous avez répondu « oui » à toutes les questions, alors, félicitations, vous avez répondu au principe de substitution de Liskov.

SOLID – O: Open/Closed

Le deuxième principe de SOLID nous apprend qu’il faut être ouvert à l’extension mais fermé à la modification.

Mais qu’est-ce que cela veut bien pouvoir dire ? Comment faire ? Est-ce que c’est possible ? Fermé aux modifications ? Cela veut dire que je ne vais plus modifier mon code ? Comme disait mon bon vieux binôme lors de mes études : Oui et non.

Oui… et non, mais pensez YAGNI et KISS!

En « open/closed », le code n’est modifié que lorsque votre client vient avec un nouveau type de demande. Qu’est-ce qu’un type de demande ? Prenons par exemple une méthode qui reçoit un « Xml » en paramètre. Votre client demande qu’il puisse donner un « Yaml » ou un « Csv ». Si vous commencez à écrire un « switch » sur le type de l’argument, qu’il faut alimenter à chaque nouvelle demande, alors vous violez ce principe. Si, par contre, vous n’avez rien à changer, c’est gagné !

Si votre client vient avec une nouvelle demande dont il en a le secret, du style : « Je veux que mon xml soit filtré, nettoyé et colorié en rouge », alors là, dans tous les cas, vous allez devoir modifier le code, faire des nouvelles méthodes, les appeler, etc…

L’essentiel est de retenir ceci : Le principe « open/closed » se limite aux faits que vous auriez déjà pu rencontrer lors de vos développements. Et surtout, surtout, surtout (surtout !), n’essayez pas d’anticiper la demande du client. Pourquoi ? Tout d’abord, parce que cela alourdit votre code et votre architecture pour « rien », vu que pour le moment, on ne l’utilise pas. Ce qui vous fera peut-être perdre du temps lors de la modification du code. Ensuite, parce qu’il faut maintenir les tests unitaires pour ces cas-là (qu’on n’utilise pas). Et enfin, parce que le client a une sorte de sixième sens, et vous demandera toujours quelque chose que vous n’aviez pas prévu.

KISS (Keep it stupid simple) et YAGNI (You aint gonna need it) sont les maitres-mots même pour le principe « open/closed ».

Ce qu’il ne faut pas perdre de vue, c’est que si le client demande une modification, c’est souvent le moment de refactoriser le code pour « préparer » les futures demandes du même type. La bonne blague ! Je vous dis de « préparer l’avenir » alors que 3 phrases plus haut, j’abordais YAGNI et KISS ? Laissez-moi une chance de m’expliquer ! La parole est à l’avocat de la couronne, c’est à vous, maitre Sneyters.

Comment ne pas procéder ?

Voici un bref résumé d’un mois de développement sur une application révolutionnaire : Le File Importer 2000. Le nom est un peu (beaucoup) has-been, mais que voulez-vous…

  • jour 0 : Votre client vous demande de prendre un csv sur un ftp distant pour l’importer dans votre application. Facile ! Votre classe se connectera sur un ftp, va récupérer le fichier distant afin de le mettre dans un dossier local. En code très simplifié, cela donne:

  • jour 14 : Vous avez fini et votre client vous dit qu’il se peut que le fichier ne soit plus sur un ftp, mais sur un compte dropbox. En bon exécutant, vous allez rajouter la notion de « dropbox » à côté du ftp.

Pour éviter une trop forte régression, j’indique que mon deuxième paramètre est optionnel. Cela m’évite de changer les appelants.

  • Jour 30 : Vos 2 types de récupération sont prêts et votre client vous dit qu’il est aussi possible qu’il le mette lui-même dans le dossier « /upload » de votre application. Pas bien grave, votre booléen se transforme en « int » sur lequel vous allez faire un switch pour savoir si on utilise le ftp, le compte dropbox ou le dossier « /upload ». Là, par contre, vous allez devoir modifier les appelants, c’est-à-dire ceux qui utilisent l’ancien système de booléen.

 

Et voilà, après un mois tout va comme sur des roulettes ! Par contre, alors que la demande est toujours du même type, à savoir « Ajouter un nouveau type d’input », vous devez modifier votre méthode, et parfois le code qui l’appelle. En tant que développeur, vous savez que modifier du code, c’est s’exposer à des problèmes de différents types:

  • problèmes de régression,
  • problèmes de maintenance de tests unitaires,
  • problèmes de « merge » de branches,
  • etc.

Comment procéder ?

Reprenons l’énoncé en mode « open/closed ».

  • Jour 0: Création de la classe. Rien ne change par rapport à la version « not open/closed » étant donné qu’on est aussi en mode KISS, on ne va rien prévoir du tout. Le code reste donc exactement:

 

  • Jour 14: Ajout de « DropBox ». C’est là qu’il faut se poser la bonne question : On nous demande que l’input puisse être, soit un ftp soit un compte « dropbox ». Faisons preuve d’un peu d’abstraction (mot-clé), et utilisons l’injection de dépendance (encore un mot clé). Ainsi, nous allons créer une interface « InputStream » qui sera implémentée par les classes « Ftp » et « DropBox ».

Notre méthode se voit doté d’un nouvel argument qui est un objet de type « InputStream ». Du coup, le code sera le même pour la classe « Ftp » et « DropBox » et ne sera plus modifié (fermé à la modification) tant que la demande ne change pas. De plus, elle fonctionnera avec d’autres types d’input qui implémente l’interface « InputStream » (elle est ouverte à l’extension).

Bien sûr, cette modification de code, en engendre une autre: Les appelants. Souvenez-vous, on faisait la même chose en mode « not open/closed », mais 15 jours plus tard (le jour 30). Il y a donc des chances pour qu’il y ait moins de code impacté.

  • Jour 30: Ajout du dossier local : On crée juste une nouvelle classe « LocalFolder » qui implémente l’interface « InputStream » et qui représente un dossier local (« /upload » dans notre cas). Le code de ma méthode n’a pas été modifié, mes tests unitaires non plus. J’ai donc peu de chance d’avoir des problèmes de régression.

Et voilà, notre classe répond bien au principe « open/closed » ! Cependant, comme je vous l’ai dit en début d’article, c’est « closed » uniquement sur ce qu’on connaît, car il est impossible d’anticiper les demandes d’un client… Il sera toujours plus malin que vous.

Dernier truc pour la route

Un petit truc, qui ne marche pas à tous les coups, mais qui peut vous aider : Lorsque le client vous demande d’ajouter une fonctionnalité, mais que vous devez modifier du code, c’est que votre code n’est peut-être pas si « open/closed ». Pour notre File Importer 2000, le client nous demande d’ajouter différents types d’input possibles, il faut donc ajouter de nouvelles classes, il ne faut pas modifier la méthode qui gère l’import. « Add new feature » n’est pas « modify existing logic ».

Ça vaut ce que ça vaut, mais ça ne coûte rien de se poser la question…