MIXIT Lyon 2024 - Le biomimétisme au secours des devs

Cette année a eu lieu la conférence MIXIT Lyon 2024, la conférence pour l'éthique et la diversité dans la tech. Au cours de ces deux journées enrichissantes, les participant.e.s ont eu l'opportunité d'explorer les sujets allant de l'inclusivité dans nos métiers IT, à la cybersécurité, en passant par l'agilité à l'échelle, la féminisation des entreprises tech et le design logiciel.

Fort de ses 25 années d'expérience, Christophe BREHERET-GIRARDIN conseille et forme les entreprises sur des sujets variés tels que le Craft, le design applicatif et le DDD (Domain-Driven Design). Sa présence sur scène lors de diverses conférences à travers l'Europe en tant que speaker lui confère une notoriété indéniable dans le domaine.

Il aborde dans sa conférence différentes problématiques du quotidien d’un·e développeur·se en nous proposant de nouvelles approches s’appuyant sur des analogies du monde de la faune et la flore.

Christophe Breheret-Girardin à MixIT Lyon 2024



Développeur•se, un métier complexe

“Bah ça va, un•e développeur•se doit juste faire quelques if par-ci par-là” — M. Smith, Product Owner sur le projet Y.

La complexité du métier de développeur·se nécessite des compétences variées pour répondre aux différentes attentes et défis rencontrés.

Gérer une complexité métier inhérente
  1. Maintenir la clarté du code : Il est crucial de maintenir un code lisible et compréhensible.
  2. Expliciter l’intention métier du code : Le code doit refléter clairement l'intention métier tout en réduisant la complexité technique au strict minimum.
  3. Éviter le sur-engineering : Il est important d'appliquer des principes comme KISS et YAGNI pour éviter de complexifier inutilement le projet.
  1. Éviter un sur-design : Le·la développeur·se doit éviter les conceptions excessivement complexes qui pourraient nuire à la valeur métier de l'application.
  2. Approche d'éternel étudiant : Il est essentiel de rester à jour avec les technologies émergentes et les meilleures pratiques. Cette approche permet de fournir un travail de qualité et de s'adapter aux évolutions rapides du secteur.
  3. Combinaison de compétences métiers et techniques : En alliant une compréhension approfondie des besoins métiers avec une expertise technique, le·la développeur·se est capable de créer des applications qui apportent une réelle valeur ajoutée.

Et le biomimétisme dans tout ça ?

“C'est imiter la nature pour en faire quelque chose”


Le biomimétisme est une approche d'innovation qui s'inspire des solutions et des processus naturels pour concevoir des technologies et des produits durables.

Le biomimétisme a déjà inspiré des inventions remarquables, telles que les panneaux solaires conçus d'après le comportement mobile des tournesols pour une meilleure captation de la lumière, ou le design du Shinkansen, le train à grande vitesse japonais, qui s'est inspiré du martin-pêcheur afin de réduire les nuisances sonores et la résistance à l'air notamment lors de la pénétration dans un tunnel.

Christophe explore comment les principes du biomimétisme peuvent s'appliquer pour résoudre efficacement les problématiques courantes dans la vie d’un logiciel.

🫚 Le figuier étrangleur - La refonte systématique

Les raisons qui poussent souvent à envisager une refonte complète d’une application sont :

  1. La dette technique accumulée : L'application est devenue difficile à maintenir et à faire évoluer.
  2. Une perte de savoir-faire : Les personnes ayant créé ou maintenu l’application ne sont plus dans l’entreprise, et la documentation est insuffisante ou inexistante.
  3. Une évolution de l’application difficile : La structure actuelle de l’application complique les ajouts de nouvelles fonctionnalités et les mises à jour nécessaires.

Une approche alternative à la refonte complète peut être comparée à celle du figuier étrangleur : cette plante grimpe autour d'un arbre hôte, remplaçant progressivement celui-ci jusqu’à le supplanter complètement.

Au lieu de refondre toute l’application d’un seul coup, l’idée est de remplacer petit à petit chaque partie obsolète ou problématique, jusqu’à ce que toutes les parties souhaitées soient modernisées :

  1. Mise en place d’une façade : créer une interface autour de la partie à remplacer et la déployer en production pour minimiser les risques.
  2. Doublage des écritures : écrire simultanément dans l’ancien et le nouveau système, tout en continuant de lire dans l’ancien. Cela permet de vérifier l’exactitude des nouvelles écritures sans perturber le fonctionnement.
  3. Transition progressive de la lecture : une fois les écritures vérifiées, permettre la lecture dans le nouveau système sans l’imposer immédiatement. Utiliser le feature flipping pour contrôler l’accès, permettant de revenir en arrière si nécessaire.
  4. Incrémentation et validation : continuer le processus de manière incrémentale, en validant chaque étape. Une fois que toutes les parties critiques sont remplacées et fonctionnent correctement, déclarer le nouveau système pleinement opérationnel et décommisionner l'ancien système.

Cette méthode itérative et prudente permet de moderniser l’application progressivement, en minimisant les risques et en s’adaptant aux besoins spécifiques à chaque étape.

“Maintenir un logiciel longtemps, c’est garder sa valeur dans le temps.” — Défier l’entropie : Refaire ou remettre sous contrôle ? 2024

🪼 La méduse immortelle - Corriger les bugs

“Arrêtez de corriger les bugs !”

Entendons-nous, corriger les bugs fait partie de la vie des développeur•euses. Ils arrivent lors de la phase de réalisation, on les corrige mais cela prend de l’énergie et du temps. Plus les bugs sont détectés tard, plus le coût est élevé : temps de correction, de tests, pour l’image de l’entreprise, etc.

Lorsque de nombreux bugs sont générés, on consacre beaucoup de temps à les corriger. Il nous reste moins de temps pour développer de nouvelles fonctionnalités. Elles sont alors réalisées rapidement au détriment de la qualité du code avec un risque d’introduire de nouveaux bugs nécessitant encore des corrections et réduisant à nouveau le temps disponible pour d'autres tâches ⇒ c’est un véritable cercle vicieux qui se met en place.

Schéma du cycle de vie d'un bug (temps passé à corriger, dépassement de délai et de budget, temps passé sur la qualité du code, anomalies à corriger)

L’addiction aux bugs – Livre blanc OCTO : Culture Code

Ce cycle infernal peut être comparable au cycle de la méduse immortelle : c’est une espèce qui, lorsqu’elle arrive à maturité sexuelle, entame une transformation cellulaire qui l’a fait redevenir plus jeune.

Pour sortir de ce cycle, devenons le prédateur de la méduse en investissant sur la qualité du code plutôt que sur la correction de bugs.

Comment ? Voici quelques pistes

  1. Questionnez vos pratiques pour améliorer la qualité : TDD (Test Driven Development), Mob/Pair programming, Code review, etc.
  2. Définissez une architecture logiciel adaptée à vos besoins : Architectures hexagonales, clean archi, etc.
  3. Améliorez la collaboration dans les équipes de développement en donnant la possibilité de questionner vos pratiques, vos process, vos standards, etc.
  4. Optez pour une stratégie de tests forte dans vos développements

”Ne soyez pas dogmatique !”

🍉 La pastèque - Tester c’est douter

La définition d’une stratégie de test est essentielle pour garantir la qualité et la fiabilité du code. Au sein d’une équipe de développement, on peut être amené à devoir atteindre une couverture de code importante (par exemple 80%) dans le but de “garantir” la qualité de notre code.

Cependant, en cherchant à atteindre cet objectif quantitatif, on risque de tomber dans le piège de l’effet cobra, où les efforts se concentrent sur l'atteinte de la métrique plutôt que sur l'amélioration réelle du code.

Mais alors, avons-nous vraiment la maîtrise de nos tests ?

Si ce n’est pas le cas, cela peut amener à différentes situations/comportements :

  1. Perte de confiance en l’application, peur du code
  2. Détection tardive des bugs, désactivation des tests
  3. Livraisons instables, augmentation du coût de maintenance

Se fier uniquement à des tests qui passent au vert peut être trompeur. Car il est malheureusement très facile de créer des tests qui semblent fonctionner en surface, mais qui en réalité ne font que survoler les véritables problèmes : des tests sans assertions, des bugs déclenchés par certaines valeurs non testées, etc.

C'est ce que Christophe compare à la pastèque : une apparence extérieure rassurante (verte) mais qui peut cacher une réalité bien moins reluisante (rouge).

Ne faisons pas des tests uniquement pour atteindre des objectifs arbitraires, mais apprenons à les maîtriser véritablement pour améliorer la qualité de notre logiciel.

Comment ?

  1. Conventions et standards d’équipe : Établir et suivre en équipe des conventions et des standards pour améliorer la lisibilité et la maintenabilité du code.
  2. Revue de code : Effectuer des revues de code régulièrement pour identifier et corriger les erreurs potentielles, enrichir les standards de l’équipe, feedbacks de l’équipe, etc.
  3. Éprouver vos tests : Mutation testing, Fuzz testing, Property-based testing

"Le test de programmes peut être une façon très efficace de montrer la présence de bug mais est désespérément inadéquat pour prouver leur absence." — Edsger W. Dijkstra

🐝 L’abeille - L’étape oubliée du TDD

Connaissez-vous l’étape la plus importante et souvent oubliée du cycle TDD ?

Pour rappel le cycle TDD commence par l’écriture d’un test qui ne passe pas, puis on écrit le minimum de code pour que ce test passe, enfin on améliore le code existant (refactorisation). C’est cette dernière étape qui est souvent oubliée. Plus l’application grandit, plus cette étape se complexifie.

Pour se représenter ces étapes du cycle TDD, Christophe s’intéresse à la danse de l’abeille. En effet, pour indiquer des sources de nourriture aux autres abeilles, une abeille fait une danse en forme de cercle. Si la nourriture est à moins de 50 mètres, l’abeille fait un cercle. Si c’est au-delà, elle va faire un huit avec plus ou moins de vitesse en fonction de la distance et de la quantité de nourriture.

Au démarrage, sur les premiers tests, il n'y a pas beaucoup de refactorisation : c’est un cycle classique du TDD. Plus on ajoute de tests, plus on ajoute du code ce qui complexifie au fur et à mesure notre application. À ce moment-là, la phase de refactorisation prend plus de temps : création de classe, mise en place d’un design Pattern émergent, etc.

Plus ce cycle de refactorisation est intense, plus on voit apparaître un 2e cycle au niveau de l’étape de refactorisation.

Schéma du double cycle TDD issu d'un article du Blog OCTO intitulé "Le double cycle TDD : ne sous-estimez plus l'étape de refactoring"Blog OCTO : Le double cycle TDD : ne sous-estimez plus l'étape de refactoring

Comment agir sur notre code lors de cette étape de refactorisation ? En travaillant sur :

  1. La lisibilité du code : le nommage, les variables, les constantes, les classes, méthodes et fonctions
  2. Les algorithmes dans notre code : simplification d’un traitement compliqué, limiter la complexité des méthodes et des classes
  3. Les tests : Les tests étant également du code, il faut en prendre soin, faciles à écrire, bien structurés et qui servent de documentation des comportements
  4. Le design applicatif : Mettre en place une architecture logicielle adaptée au besoin, utiliser des principes de design comme les principes S.O.L.I.D

C’est quoi les principes S.O.L.I.D ?

🦗 Le Criquet migrateur - S.O.L.I.D

S pour Single Responsibility Principle (SRP) : Une classe doit avoir une et uniquement une seule raison de changer.

L'objectif est de rendre le code plus maintenable et flexible en encourageant un découpage clair des méthodes. En théorie, ce principe est facile à comprendre, mais en pratique, il s'avère plus complexe à suivre. Par exemple, il est courant de rencontrer des méthodes trop longues, une confusion sur les responsabilités de chaque classe ou, à l'inverse, une sur-fragmentation du code qui le rend plus difficile à gérer.

Finalement ce qu’on cherche à faire c’est rassembler ce qui “vit” ensemble et de découper un problème complexe en petits sous-problèmes.

C'est ce que Christophe compare aux criquets migrateurs, une espèce qui se déplace en énorme essaim. Pour se nourrir ils ravagent les champs, les premiers mangent tout et les derniers n’ont plus rien ⇒ pour faire face à ce problème ils se divisent pour survivre.

Il faut appliquer ce principe de division à notre code.

Comment ?

  1. Avec une architecture qui donne une structure qui aide à diviser/facilite la division : architecture hexagonale, clean architecture
  2. Arborescence adaptée : bounded context, découpage en périmètre applicatif, slicing (vertical vs horizontal)

Astuces 💡

Entrainez-vous au refactoring avec le kata “Gilded rose

💐 La singularité de l’orchidée - S.O.L.I.D

O pour Open-Closed Principle (OCP) : Une classe doit être à la fois ouverte (à l'extension) et fermée (à la modification). Ouverte signifie qu’une classe doit pouvoir être étendue. Fermée signifie qu’une classe ne peut être modifiée que par extension sans modification de son code.

Pour illustrer ce principe, Christophe s’appuie sur la singularité de l’orchidée : en effet, il existe plus de 850 genres d’orchidées ayant chacune des caractéristiques propres (période de floraison, les pollinisateurs, le besoin en eau, etc.).

Imaginons que nous voulions écrire un programme qui décrit l’arrosage d’une orchidée par un horticulteur suivant une température.


public class Labiata implements Orchidee {

  private GenreOrchidee genre = GenreOrchidee.CATTLEYA;

  @Override

  public GenreOrchidee getGenreOrchidee() {

    return genre;

  }

}

public class Horticulteur {

  public void arroserSiBesoin(Orchidee orchidee, Temperature temperature) {

    switch(orchidee.getGenreOrchidee()) {

      case PHALAENOPSIS: {

	  // Code d’arrosage en fonction de la température

      }

      case CATTLEYA: {

	  // Code d’arrosage en fonction de la température

      }

    }

  }

}


Actuellement, il n’y a que deux genres d'orchidées, mais imaginons que nous voulions ajouter un nouveau genre d'orchidées ? Dans notre cas nous devrions alors ajouter un nouveau case dans le switch et modifier la classe horticulteur. Le principe n’est donc pas respecté.

Comment respecter dans ce cas le principe d’Open/Close ?


public class Horticulteur {

  public void arroserSiBesoin(Orchidee orchidee, Temperature temperature) {

    if(orchidee.aBesoinDArrosage(temperature)) {

      arroser(orchidee);

    }

  }

  private void arroser(Orchidee orchidee) {

    // Code d’arrosage

  }

}

public class Labiata implements Orchidee {

  private GenreOrchidee genre = GenreOrchidee.CATTLEYA;

  @Override

  public GenreOrchidee getGenreOrchidee() {

    return genre;

  }

  @Override

  public boolean aBesoinDArrosage(Temperature temperature) {

    return genre.getCaracteristiques().aBesoinDArrosage(temperature);

  }

} 

En ajoutant la notion de caractéristiques par genres d'orchidées, pour définir pour chaque genre d’orchidées les caractéristiques nécessaires pour déterminer s' il y a besoin d’arrosage en fonction de la température.


public interface CaracteristiquesOrchidee {

  boolean aBesoinDArrosage(Temperature temperature);

}

public enum GenreOrchidee {

  PHALAENOPSIS(new CaracteristiquesGenrePhalaenopsis()),

  CATTLEYA(new CaracteristiquesGenreCattleya());

  private final CaracteristiquesOrchidee caracteristiques;

  private GenreOrchidee(CaracteristiquesOrchidee caracteristiques) {

    this.caracteristiques = caracteristiques;

  }

  public CaracteristiquesOrchidee getCaracteristiques() {

    return caracteristiques;

  }

} 

Si nous ajoutons un nouveau genre d'orchidées, on ne peut pas oublier de définir ses caractéristiques grâce à cette implémentation.

Au final, l’horticulteur n’est pas modifié :

  • Prise en charge de n’importe quel genre d’orchidée
  • Fermé à la modification et ouvert à l’extension

Astuces 💡

Signes d’un potentiel besoin d’OCP ?
- Une série de “if”
- Un switch case

🦎 Le caméléon - S.O.L.I.D

L pour Liskov Substitution Principle : “Si q(x) est une propriété démontrable pour tout objet x de type T, alors q(y) est vraie pour tout objet y de type S tel que S est un sous-type de T”

A la base c’est un principe mathématique qui peut s’appliquer au code. A première vue, ce principe est plus difficile à comprendre et à appliquer dans le code.

Mais alors comment l’expliquer facilement ? Grâce au caméléon : En effet, le caméléon est un animal qui est très adaptable, il peut, chez certaines espèces, notamment changer de couleur pour se camoufler ou séduire. Chaque espèce de caméléon possède une palette de couleur en fonction de la région où ils vivent.

Imaginons que nous voulions écrire une application Tinder dédiée aux caméléons. Pour séduire, certaines espèces de caméléon peuvent changer de couleur pour attirer son/sa partenaire.

Une première proposition : Nous avons une interface Cameleon, et une classe pour chaque espèce de caméléon.


public class Tinder {

  public void changerCouleurPourSeduire(Cameleon cameleon) {

    if(cameleon instanceof CameleonPanthere) {

      cameleon.changeCouleur(List.of(Couleur.BLEU, Couleur.ROUGE, Couleur.JAUNE, Couleur.ORANGE));

    } else if(cameleon instanceof CameleonVoileDuYemen) {

      cameleon.changeCouleur(List.of(Couleur.VERT, Couleur.BLEU, Couleur.JAUNE));

    } else if(cameleon instanceof CameleonNainDesert) {

      // Ne change pas de couleur

    }

  }

}

Dans cette architecture, la classe Tinder est couplée avec le fonctionnement interne de chaque Caméléon puisque nous avons à gérer chaque espèce directement dans la classe Tinder. Les caméléons ne sont pas substituables entre eux. Le principe n’est donc pas respecté.

Comment respecter dans ce cas le principe de Liskov ?


public class Tinder {

  public void seduire(Cameleon cameleon) {

    cameleon.declencherModeSeduction();

  }

}

public class CameleonPanthere implements Cameleon {

  private List<Couleur> couleurs;

  @Override

  public void declencherModeSeduction() {

    couleurs = List.of(Couleur.BLEU, Couleur.ROUGE, Couleur.JAUNE, Couleur.ORANGE);

  }

}

public class CameleonVoileDuYemen implements Cameleon {

  private List<Couleur> couleurs;

  @Override

  public void declencherModeSeduction() {

    couleurs = List.of(Couleur.VERT, Couleur.BLEU, Couleur.JAUNE);

  }

}

Tous les caméléons sont maintenant interchangeables :

  • La formule mathématique est respectée
  • Chaque caméléon s’adapte suivant sa propre spécificité

Astuces 💡

Chercher à connaître l’implémentation d’une classe pour agir en conséquence :
- “instanceof” en Java, TS, JS, PHP
- “is” en C#
- “isinstance” en Python

🍃 La diversité de la prairie - S.O.L.I.D

I pour Interface Segregation Principle (ISP) : C’est le principe de ségrégation des interfaces, il consiste à bien découper son code avec des interfaces spécifiques et adaptées selon les besoins pour :

  • Une meilleur compréhension du code
  • Éviter qu’une classe ne dépende de méthodes qu'elle n’utilise pas

Pour illustrer ce principe, Christophe nous propose de nous intéresser à la diversité de la prairie : c’est un espace où toutes les plantes peuvent coexister. Elles sont plus ou moins denses, avec des racines plus ou moins profondes, des feuilles plus ou moins grandes et puis des méthodes de reproduction différentes.


interface CaracteresBotaniquesFleuris {

  void fournirEspace();

  void enfoncerRacines();

  void diffuserOdeur();

  void produireNectar();

}

public class Herbe implements CaracteresBotaniquesFleuris {

  @Override

  public void fournirEspace() { 

    [...]

  }

  @Override

  public void enfoncerRacines(){ 

    [...]

  }

  @Override

  public void diffuserOdeur() { // Ne rien faire }

  @Override

  public void produireNectar() {

    throw new UnsupportedOperationException(“L’herbe ne produit pas de nectar”);

  }

}

L’herbe ne produisant pas de nectar ni d’odeur, elle ne devrait pas implémenter les méthodes diffuserOdeur et produireNectar. Le principe n’est pas respecté.

Comment respecter, dans ce cas, le principe d’Interface Segregation ?

Découpons notre interface CaracteresBotaniquesFleuris en plusieurs interfaces.


interface TerrainDePrairie {

  void fournirEspace();

}

interface Racines {

  void enfoncerRacines();

}

interface Parfum {

  void diffuserOdeur();

}

interface ProducteurNectar {

  void produireNectar();

}

public class Herbe implements TerrainDePrairie, Racines {

  @Override

  public void fournirEspace() { 

    // L’herbe couvre une bonne partie du terrain de la prairie

    [...]

  }

  @Override

  public void enfoncerRacines(){ 

    // Les racines de l’herbe s’étendent superficiellement dans le sol

    [...]

  }

}

”N’obligez pas les classes à définir des méthodes qu’elles ne savent pas implémenter”


Astuces 💡

Quelques signes pour nous aider à détecter un besoin d’ISP :
- Méthode qui ne fait rien
- Méthode qui lève simplement une exception

🌺 L’abeille et la fleur - S.O.L.I.D

D pour Dependency Inversion Principle (DIP) : C’est le principe d’inversion de dépendance : les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, mais tous deux doivent dépendre d'abstractions. Ces abstractions ne doivent pas dépendre des implémentations concrètes, mais les implémentations concrètes doivent dépendre des abstractions.

Pour illustrer ce principe, Christophe nous propose de nous intéresser à l’abeille et à la fleur. La fleur diffuse une odeur pour attirer l'abeille qui, en se posant sur plusieurs fleurs, récupère et dépose du pollen, permettant ainsi la pollinisation, la reproduction des fleurs et la formation de fruits et de graines.


public class Abeille {

  private List<Pollen> pollens = new ArrayList<>();

  private Marguerite marguerite = new Marguerite();

  public void butiner() { 

    pollens = marguerite.echangeDePollen(new ArrayList<>(pollens));

  }

}

Dans ce cas, l’abeille ne sait butiner qu’une marguerite. Mais que se passe-t-il si on veut que l’abeille butine une autre variété de fleurs ? Il y a un couplage fort entre l’abeille (le module de haut niveau) et la marguerite (le module de bas niveau). Le principe n’est pas respecté.

Comment respecter, dans ce cas, le principe de Dependency Inversion ?

Ajoutons une couche d’abstraction !


public interface Fleur {

  List<Pollen> echangeDePollen(List<Pollen> pollens);

}

public class Marguerite implements Fleur {

  @Override 

  public List<Pollen> echangeDePollen(List<Pollen> pollens) {

    // Code d’échange de pollen

  }

}

public class Abeille {

  private List<Pollen> pollens = new ArrayList<>();

  private Fleur fleur;

  public Abeille(Fleur fleur) {

    this.fleur = fleur;

  }

  public void butiner() { 

    pollens = fleur.echangeDePollen(new ArrayList<>(pollens));

  }

}


Il y a maintenant un couplage faible, l’abeille ne dépend plus d’une implémentation mais d’une abstraction, elle est maintenant capable de butiner n’importe quelle fleur.

Astuces 💡

Signes d’un potentiel besoin de DIP ?
- Difficulté à tester les modules individuellement
- Trop de mocks dans les tests
- Lorsque la modification des modules de bas niveau entraînent des modifications dans les modules de haut niveau

Take away

Devenez un·e développeur·se "augmenté" en vous inspirant du biomimétisme. Utilisez les méthodes et les principes tirés de la nature pour construire de meilleures applications :

  • Ne remplacez pas toute une application inutilement, mais concentrez-vous sur le remplacement de parties spécifiques.
  • Adoptez les concepts présentés pour créer des applications maintenables et évolutives.
  • Observez et respectez la nature, en imitant ses principes pour de bonnes raisons. Ne suivez pas aveuglément les tendances comme les microservices ou d'autres pratiques parce que des géants comme Netflix ou Spotify le font, mais faites ce qui est nécessaire et utile pour vos besoins.

"L'idéal de la vie n'est pas l'espoir de devenir parfait·e, mais la volonté de devenir toujours meilleur·e." — Ralph Waldo Emerson

Références

Quelques références sur des concepts abordés dans l’article, mais qui peuvent également aider à s’améliorer dans la pratique de TDD :