Point d'inflexion

Inflection Point Illustration

Traduction de l'article de Sandro Mancuso "Inflection Point" (Article original)

Nous disons tous que la conception de logiciels est une question de compromis, mais comment raisonnons-nous réellement à ce sujet ? Comment décidons-nous de la quantité de code que nous allons écrire pour une tâche donnée ? Est-ce que l’approche directe (la chose la plus simple qui puisse fonctionner) est la bonne approche ? Le plus simple ? Comment faire la différence entre simple et facile ? Est-ce même la bonne question ?

D'une manière ou d'une autre, inconsciemment ou non, nous prenons toujours la décision de commencer à implémenter un nouveau comportement de quelque part. Certains d'entre nous écrivent juste assez de code pour satisfaire le nouveau comportement. D'autres écrivent beaucoup plus de code, essayant d'éviter de futures retouches au cas où les choses changeraient ou évolueraient. Beaucoup d'autres sont quelque part entre les deux.

Supposons ce qui suit :

Point d'inflexion

Sur le côté gauche, nous avons la solution la plus simple pour un comportement souhaité donné. Sur le côté droit, nous avons des possibilités infinies pour écrire du code à l'épreuve du futur (« future proof code » dans la version originale). Le problème est qu’il est impossible, avec le côté droit, d’écrire du code à l'épreuve d’un futur constitué de possibilités infinies. Mais nous pouvons, cependant, choisir une capacité logicielle et essayer d'écrire un code "futur proof" pour cela.

Par exemple :

  • Nous pouvons avoir besoin de mettre à l'échelle cette application
  • Nous pouvons avoir plusieurs utilisateurs avec différents niveaux d'accès.
  • Nous devrons le rendre très sécurisé
  • Nous pouvons ajouter plus de clients (mobile, web, autres systèmes)
  • Nous pouvons avoir différents types de paiements (ou d'offres, ou de voyages, ou d'hébergement, etc.)
  • Nous pouvons souhaiter l'imprimer sur la console, générer un PDF ou appeler un système de reporting.
  • Nous souhaitons peut-être fournir une API afin de nous préparer à de futures intégrations avec d'autres systèmes.

Bien que nous puissions essayer, écrire du code qui peut rester flexible pendant toute la durée de vie d'un projet est pratiquement impossible. Vous vous tromperez, quoi que vous fassiez. En plus de cela, vous ajouterez de la complexité partout car il n'y a aucun moyen de savoir avec certitude quelles zones de notre base de code vont évoluer.

Je ne pense pas qu'il existe une solution ou une ligne directrice claire pour ce problème, mais il y a, au moins, une meilleure façon de raisonner à ce sujet. Je l'appelle point d'inflexion.

Le point d'inflexion définit le montant maximum d'investissement que nous sommes à l'aise de faire dans un type de flexibilité souhaité à un moment donné. Au-delà de ce point, l'investissement ne vaut plus.

Il y a deux manières de raisonner sur le point d'inflexion :

  • De droite à gauche : nous choisissons une capacité logicielle que nous jugeons très importante dans un avenir proche. Nous réfléchissons ensuite à quelle serait notre solution idéale pour cette capacité logicielle. À partir de ce point, qui peut être assez loin du côté droit, nous commençons à réfléchir à la façon dont nous pourrions rendre notre solution plus simple (probablement aussi moins chère et plus rapide à mettre en œuvre) dès maintenant sans perdre de vue la flexibilité que nous aimerions avoir dans le futur. Nous continuons à discuter de la façon dont nous pouvons simplifier la solution jusqu'à ce que nous trouvions un point où la simplifier signifiera perdre tellement de flexibilité qu'il sera trop coûteux d'évoluer vers notre solution idéale à l'avenir.
    C'est le point d'inflexion, venant de droite à gauche.
  • De gauche à droite : nous partons de la solution la plus simple pour un problème donné. Nous réfléchissons ensuite à ce que nous savons actuellement du projet, aux fonctionnalités qui seront certainement implémentées ensuite et aux fonctionnalités logicielles importantes qui devront être fournies dans un avenir proche. Avec cette connaissance, nous pouvons décider de la manière dont nous pourrions rendre notre code flexible dès maintenant, passant d'une solution simple à une solution plus sophistiquée jusqu'à un point où le coût de la flexibilité que nous fournissons en ce moment ne vaut tout simplement pas l'effort ou est trop spéculatif et risqué.
    C'est le point d'inflexion venant de gauche à droite.

Le point d'inflexion comme ligne directrice générale

Regardons quelques scénarios courants. Abordez-les avec du recul car il existe de nombreux autres facteurs qui pourraient les rendre invalides ou faire basculer le point d'inflexion d'un côté différent. De plus, l'expérience de l'équipe dans certaines technologies et approches aura également un impact sur ce qui est considéré comme simple et sur le coût d'une flexibilité accrue.

Exemple 1 : séparation de la structure de page et des données sur une application Web

Devrions-nous séparer une structure de page Web (HTML) de ses données ou devrions-nous demander à notre backend de générer la page entière via un moteur de modèle et de renvoyer la page entière en une seule fois ?

Que sommes-nous en train d’optimiser en avance ? Quel type de flexibilité voulons-nous offrir maintenant et quel impact cela aurait-il dans notre code ?

La séparation de la structure de la page et des données peut apporter des avantages pour des chantiers futurs comme la mise en place d’une API RESTful stable pour d'autres clients (mobiles, autres systèmes). Cela pourrait également faciliter la rédaction de tests automatisés de notre application. En plus de cela, nous pouvons améliorer l’expérience utilisateur puisque la page se chargera plus rapidement ou ne se rechargera même pas du tout, en fonction de l'implémentation (page unique / multi). Pour y parvenir, nous aurons probablement besoin d'utiliser des langages de programmation différents pour le Frontend et le Backend. Il faut fournir plusieurs méthodes de contrôleur dans le Backend, convertir des objets en JSON. Il y aura plus de code à dans le Front et le Back, et quelques couches de traduction de données.

Une approche différente consisterait à utiliser un moteur de template et à effectuer le rendu de toutes les pages sur le serveur. Cela pourrait être « plus facile » car les bibliothèques de moteurs de modèles sont assez matures dans la plupart des principaux langages et nous gardons toute l’application écrite dans un seul langage de programmation. Pour les développeurs backend, cela peut être plus simple.

Mais que perdons-nous ? Avons-nous une pire expérience utilisateur ? Eh bien, peut-être. La connexion Internet est aujourd'hui beaucoup plus rapide qu'elle ne l'était il y a 10-15 ans, alors que l'utilisation d'AJAX était un must (et aussi assez difficile - la guerre des navigateurs n'importe qui ?). Qu'en est-il de la flexibilité pour ajouter de nouveaux clients (mobiles / autres systèmes) ? Pouvons-nous vraiment prévoir de quel type d'API ils auront besoin ? Qu'en est-il de la granularité de l'API ? Serait-ce le même que celui utilisé pour le Web ?

Ensuite, nous avons des choses comme les compétences d'équipe. Dans quelle mesure connaissons-nous toutes les technologies impliquées ? Dans quelle mesure les plans de mise en oeuvre d'une application mobile sont-ils concrets ? Est-ce qu'une application native ou une page Web responsive conviendrait ?

Avons-nous une équipe de conception Web / UX distincte ? Quelle approche serait la plus simple pour les intégrer à l'équipe et travailler sur la même base de code ?

Où est le point d'inflexion ? Quelle complexité devrions-nous ajouter maintenant ? Un code supplémentaire est-il vraiment un gros problème pour la quantité de flexibilité que nous obtenons, même s'il est un peu spéculatif ? Existe-t-il d'autres alternatives pour offrir une flexibilité similaire sans autant de code supplémentaire ? Combien paierions-nous si nous retardons la décision d'offrir la flexibilité dès maintenant ?

Exemple 2 : Connaître les fonctionnalités à venir

Lorsque notre travail est à visée exploratoire, je vous conseillerais fortement de rechercher la solution la plus simple. Cependant, lorsque nous travaillons dans un environnement où nous avons un Product Owner avec une vision claire, un backlog de produits soigneusement entretenu et un budget suffisamment important pour garantir que le projet se déroulera pendant plusieurs mois, voire des années, devons-nous systématiquement viser la solution la plus simple ?

Imaginez que nous travaillons sur la fonctionnalité A et que nous savons que les fonctionnalités B et C sont les prochaines fonctionnalités à implémenter. Imaginez également qu'elles sont étroitement liées, cela signifie que les fonctionnalités B et C seront construites en plus de l'implémentation de la fonctionnalité A.

Dans ce scénario, devrions-nous viser la solution la plus simple pour la fonctionnalité A, puis tout refactoriser afin d'ajouter les fonctionnalités B et C ? Jusqu'où allons-nous avec la mise en œuvre de la fonctionnalité A lorsque nous sommes sûrs à 99% que les fonctionnalités B et C vont être implémentées immédiatement après avoir terminé la fonctionnalité A ? Mais que se passerait-il si nous n'étions sûrs qu'à 50% ? ou 20% ? Où serait le point d'inflexion ?

Exemple 3 : Couches

De nombreux développeurs utilisent une sorte d'architecture en couches. Une couche commune serait la couche de données qui est normalement définie par les classes « repository ».

En près de 20 ans de carrière, je n'avais qu'une seule application pour laquelle nous avons en fait changé notre mécanisme de persistance et la couche de « repositories » était extrêmement utile. Le passage d'une base de données à une autre n'avait pratiquement aucun impact sur le reste du code.

J'ai récemment eu quelques discussions sur cette couche de données (référentiel). Certains de mes collègues ont dit que cette complexité supplémentaire n'est pas toujours payante puisque nous n'allons probablement pas changer le mécanisme de persistance. C'est un commentaire juste et normalement la couche de « repositories » peut être considérée comme du code à l'épreuve du futur et non comme la solution la plus simple.

Mais quelle est l'alternative ? Active Record ? La logique de persistance est-elle mélangée à la logique d'application ? Violation de SRP ? Pas de séparation des préoccupations ? Un autre type de séparation qui sera très similaire aux référentiels mais moins explicite ? Utilisation d'un framework ? Comment cela affecte-t-il les limites transactionnelles ? Devraient-ils se trouver dans la couche de « repositories » ou devraient-ils être aux points d'entrée de votre modèle de domaine ?

Voilà un exemple où discuter de la solution la plus simple par rapport au type de code à l’épreuve du futur que nous souhaitons avoir peut différer beaucoup d'une équipe à l'autre. Certains trouveraient une couche de « repositories » un prix très bon marché à payer tandis que d'autres la trouveraient trop chère.

Donc, au lieu de discuter de la couche référentielle, nous devrions discuter du type de flexibilité que nous aimerions avoir en matière de persistance et combien sommes-nous prêts à payer pour cela maintenant. Quel serait le point d'inflexion ?

Exemple 4 : Architecture et conception haut-niveau

Doit-on démarrer une application avec un monolithe ou avec un tas de micro-services ? Ou quelque part entre les deux ? Services normaux ? Modules d'application ? Structure de package / espace de noms bien définis ? Ce n'est pas une question simple et le point d'inflexion changera considérablement selon le contexte. Travaillons-nous pour une petite startup avec deux développeurs ? Travaillons-nous avec une grande entreprise avec un budget pour un projet pluriannuel, qui débutera avec 50 développeurs dès le premier jour ? Que savons-nous du domaine ? Explorons-nous une idée ? Ou est-ce un domaine bien défini ?

Pour quoi optimisons-nous ? Combien ça coûte ? Quelle est la complexité de la solution ? Quels sont les compromis ? Devrions-nous pré-optimiser pour l'évolutivité ou devrions-nous nous concentrer sur le plus simple ? Que perdons-nous ou gagnons-nous avec une solution donnée ? Pouvons-nous reporter cette décision à une étape ultérieure ? Serait-ce trop chaotique d'avoir tous les développeurs travaillant sur la même base de code ? Quelles capacités logicielles voulons-nous avoir dans la première version ? Quel serait le coût de certaines décisions architecturales à l'avenir ?

J'ai travaillé dans des projets où il était logique de commencer petit et de développer l'application petit à petit, en me concentrant sur les solutions les plus simples au début. Mais j'ai aussi travaillé dans des applications beaucoup plus larges où il était trop risqué ou presque impossible de différer certaines décisions architecturales. Le point d'inflexion était complètement différent dans ces deux contextes.

Exemple 5 : Micro-conception

Beaucoup conviennent que l'utilisation de primitives pour représenter des concepts de domaine est mauvaise. Alors, devrions-nous créer des types pour tout ? Quel est le coût et combien de code supplémentaire devons-nous écrire ? La quantité de code est-elle la seule préoccupation ? Essayons-nous de réduire le nombre de bogues à l'avenir ?

Quel prix payons-nous lorsque nous créons des types pour tout ? Améliore-t-il vraiment la lisibilité ? Quels gains avons-nous à l'avenir ? Devrions-nous limiter la création de type à notre modèle de domaine ? Quand est-il possible d'utiliser des primitives ? Quel est l'impact sur la maintenabilité et les tests ? Aurions-nous besoin d'écrire plus ou moins de tests lors du choix des primitives sur les types ? Est-il facile de créer des types dans le langage de programmation choisi ?

Ce sont de nombreuses considérations qui peuvent définir l'emplacement du point d'inflexion.

Conclusion

La sur-ingénierie a un coût élevé et peut causer beaucoup de dégâts. Cependant, une longue séquence de solutions simples peut également causer beaucoup de douleur et de retouches à mesure que le système se développe. Chaque changement devient une énorme tâche de refactoring.

En règle générale, je préfère d'abord regarder quelle serait la solution la plus simple, puis commencer à explorer quelques possibilités pour offrir plus de flexibilité pour les changements futurs étant donné une capacité logicielle importante (de gauche à droite). De plus, approche directe (la plus simple) ne veut pas dire « quick and dirty » (rapide et sale).

Cependant, il y a des moments où nous savons que certaines choses sont très importantes et les considérer dans les premières étapes d'un projet, ou lors de la création d'une nouvelle fonctionnalité, peut être vraiment bénéfique. Peut-être que le prix que nous payons maintenant pour un code supplémentaire peut être considéré comme une bonne affaire par rapport à la quantité de flexibilité que nous gagnerons à l'avenir, ce qui déplacera le point d'inflexion davantage vers la droite.

Chaque fois que vous avez une discussion de conception avec vos pairs ou votre équipe, concentrez-vous sur la recherche du point d'inflexion. Cela rendra la discussion plus objective. Au lieu de « mon idée contre la vôtre » ou « cette approche contre cette approche », nous devrions discuter du type de flexibilité que nous aimerions avoir et de la façon dont nous pouvons y parvenir sans payer de prime. Combien sommes-nous prêts à payer ? Devrions-nous payer maintenant ou dans le futur ? Lointain ou proche avenir ? Pouvons-nous payer en plusieurs fois ?

Essayer de répondre aux questions ci-dessus nous aidera à raisonner sur nos décisions et à trouver un bon point de départ (point d'inflexion) pour de nouveaux projets ou fonctionnalités.

Pour aller plus loin

Podcast "Artisan Développeur" / Episode "Pourquoi attendre ?"

Episode du 19/11/2019

https://artisandeveloppeur.fr/pourquoi-attendre/

Accessible depuis le blog et sur la plupart des plateformes de streaming.

Voici le lien sur Deezer : https://deezer.page.link/w3j1BNrisGAPaz656

Pour plus d'informations concernant son blog, c'est par ici.

Biais de complexité

http://amaninthearena.com/biais-de-complexite/

Compliqué ou complexe ?

https://www.cogito-conseil.fr/complique-ou-complexe/

Introduction aux systèmes complexes : Le modèle Cynefin

http://amaninthearena.com/systemes-complexes/