- Dès qu’il est écrit, le code engendre un coût de maintenance ; il est donc souvent plus important de le structurer pour qu’il soit facile à supprimer ou à remplacer plus tard que pour le rendre réutilisable
- Plus une API a d’utilisateurs, plus son coût de modification augmente ; plus la dépendance à une API tierce est profonde, plus les changements externes secouent fortement la base de code
- Duplication, boilerplate, layering, gros blocs de code, séparation en modules et feature flags sont tous, selon le contexte, des outils de gestion des dépendances
- Une bonne séparation ne consiste pas tant à regrouper des fonctionnalités communes qu’à masquer les unes aux autres les décisions de conception difficiles à changer ou susceptibles de changer
- Un bon code n’est pas un code parfait dès le départ, mais un code legacy qui gêne moins avec le temps ; au fond, c’est un code facile à supprimer
Le code est un coût, et supprimer du code réduit ce coût
- Tout code crée, dès son écriture, un coût de maintenance ; la réutilisation peut réduire le volume de code, mais aussi rendre plus difficile le changement d’avis par la suite
- Plus il y a de code qui utilise une API, plus modifier cette API implique de réécriture
- Plus on dépend fortement d’une API tierce, plus on est affecté lorsque cette API change
- Dans les grands systèmes, comprendre comment les morceaux de code s’emboîtent et quelles parties dépendent des autres devient un problème de plus en plus difficile avec le temps
- Si l’on considère les lignes de code non comme des « lignes produites » mais comme un « coût engagé », supprimer du code revient à réduire le coût de maintenance
- L’objectif n’est pas seulement de créer du logiciel réutilisable, mais du logiciel jetable
Étape 0 : ne pas écrire de code
- Le nombre de lignes de code ne dit pas tout, mais l’échelle compte : 50 lignes, 500 lignes, 5 000 lignes, 10 000 lignes, 25 000 lignes
- Remplacer un monolithe d’un million de lignes demande plus de temps, d’argent et d’efforts qu’un monolithe de 10 000 lignes
- Plus il y a de code, plus il est difficile de s’en débarrasser, mais supprimer une seule ligne ne fait quasiment rien économiser
- Le code le plus facile à supprimer est celui qu’on n’a jamais écrit
Étape 1 : faire du copier-coller
- Il est plus facile de créer du code réutilisable après l’apparition de plusieurs cas d’usage réels que de l’anticiper à l’avance
- Faire plusieurs copier-coller dans une base de code permet de bien comprendre les usages réels
- Dès qu’un morceau de code devient une API partagée, il devient difficile à modifier
- Le code qui appelle une fonction dépend non seulement de son comportement documenté, mais aussi des comportements intentionnels ou non observés dans son implémentation
- Supprimer le code interne d’une fonction est plus simple que supprimer la fonction elle-même
Étape 2 : arrêter le copier-coller
- Lorsqu’un code a été suffisamment répété, il est temps de l’extraire dans une fonction
- Cela concerne le code de type utilitaire, souvent nécessaire au-dessus de la bibliothèque standard, comme du code qui ouvre un fichier de configuration et renvoie une table de hachage, ou du code de suppression de répertoires
- Il vaut mieux faire de
utilun répertoire plutôt qu’un seul fichier, et placer les différents utilitaires dans des fichiers séparés- Un fichier
utilunique continue de grossir, et devient difficile à découper une fois trop volumineux
- Un fichier
- Le code moins spécifique à une application ou à un projet est plus facile à réutiliser et moins susceptible de changer ou d’être supprimé
- C’est le cas du code de bibliothèque pour le logging, les API tierces, les descripteurs de fichiers ou les processus
- Les listes, tables de hachage et collections sont rarement supprimées, non seulement grâce à leur interface simple, mais aussi parce que leur périmètre ne s’élargit pas avec le temps
- L’essentiel est d’éloigner autant que possible les parties difficiles à supprimer des parties faciles à supprimer
Étape 3 : écrire davantage de boilerplate
- Créer une bibliothèque permet d’éviter le copier-coller, mais en pratique on finit souvent par écrire beaucoup de boilerplate pour utiliser cette bibliothèque
- Le boilerplate ressemble au copier-coller, dans la mesure où l’on modifie à chaque fois quelques emplacements différents
- Cette duplication consiste à accepter de la verbosité en échange de moins de dépendances et de plus de flexibilité
- Les bibliothèques qui nécessitent du boilerplate relèvent souvent de cas où il est difficile de mélanger politique et protocole, comme les protocoles réseau, les formats filaires ou les outils de parsing
- Le protocole concerne ce que le programme peut faire
- La politique concerne ce que le programme doit faire
- Ce code est souvent difficile à supprimer, car il répond à des exigences de communication avec d’autres ordinateurs ou de manipulation d’autres fichiers
- Il est important de ne pas disperser la logique métier dans ce code
- Même si cela ajoute des lignes, mieux vaut les écrire dans les parties faciles à supprimer
Étape 4 : ne pas écrire de boilerplate
- Quand le boilerplate devient trop abondant, il est temps de créer une bibliothèque qui enveloppe la bibliothèque flexible avec des opinions sur la politique, le workflow et l’état
- Créer une API facile à utiliser revient souvent à bibliothécariser le boilerplate
- Le client HTTP Python
requestsest un exemple d’interface simple fournie au-dessus deurllib3, plus verbeuxrequestscouvre les workflows courants d’utilisation de HTTP et masque les détails pratiquesurllib3fournit le pipelining, la gestion des connexions, etc., sans cacher les détails à l’utilisateur
- Envelopper une bibliothèque dans une autre ne consiste pas seulement à masquer des détails : c’est aussi une séparation des préoccupations
- Il vaut mieux ne pas mettre de logique métier dans le répertoire
util, et empiler une bibliothèque facile à utiliser au-dessus d’une bibliothèque simple à implémenter - Il peut aussi être préférable d’envelopper les bibliothèques tierces
- Pour éviter que tout le projet soit verrouillé sur un choix donné, on peut créer une bibliothèque adaptée à son propre code
- Les API faciles à utiliser et les API extensibles entrent souvent en conflit
- Le layering consiste moins à écrire du code que l’on supprimera plus tard qu’à rendre agréable à utiliser du code difficile à supprimer, sans le contaminer avec la logique métier
Étape 5 : écrire de gros blocs de code
- Malgré le copier-coller, le refactoring, le layering et la composition, le code doit bien finir par faire quelque chose ; il faut donc parfois un gros bloc de code qui maintient le reste ensemble
- La logique métier peut se caractériser par une infinité de cas limites et de hacks rapides
- Le code de jeu ou le code de fondateur relève du même genre : du code qui prend des raccourcis pour économiser beaucoup de temps
- Il est parfois plus facile de supprimer une grosse erreur que d’éliminer 18 petites erreurs entremêlées
- Une grande partie de la programmation est exploratoire ; il peut donc être plus rapide de se tromper plusieurs fois et d’itérer que d’essayer de viser juste dès le départ
- Quand on crée son premier jeu, il vaut mieux ne pas commencer par créer un moteur ; de même, il vaut mieux ne pas créer un framework web avant d’écrire une application
- Le monorepo est un compromis similaire
- Il est difficile de savoir à l’avance comment découper le code, et une seule grosse erreur est plus facile à déployer que 20 éléments fortement couplés
- Si l’on sait qu’un code sera bientôt jeté, supprimé ou facilement remplacé, on peut se permettre davantage de raccourcis
- L’objectif n’est pas de répéter dix fois la même boule de boue pour perfectionner l’erreur, mais de faire à chaque fois de nouvelles erreurs, de prendre de nouveaux risques et de construire par itération
- Les projets finissent soit par échouer, soit par devenir du code legacy, et l’échec arrive plus souvent que le succès
- Il est plus facile de supprimer un tout que de supprimer du code morceau par morceau
Étape 6 : découper le code en morceaux
- La grosse boule de boue est la plus facile à créer, mais celle dont le coût de maintenance est le plus élevé
- Une modification qui paraît simple peut obliger à toucher presque toute la base de code avec des rustines
- Un code qui était facile à supprimer dans son ensemble devient difficile à supprimer morceau par morceau
- Les modules devraient être découpés non selon les fonctionnalités communes, mais selon ce qui n’est pas partagé avec le reste et les décisions de conception qu’il faut masquer
- Comme dans le critère de D. Parnas, on peut lister les décisions de conception difficiles ou susceptibles de changer, puis concevoir chaque module pour cacher ces décisions aux autres modules
- Les modules ne sont pas faits pour la réutilisation, mais pour la capacité à changer
- Le principe de responsabilité unique peut se comprendre comme « chaque module ne doit traiter qu’un seul problème difficile », mais le plus important est que « chaque problème difficile ne doit être traité que dans un seul module »
- Si un module fait deux choses, c’est souvent parce qu’il faut modifier l’autre partie lorsque l’on veut en changer une
- Un composant affreux avec une interface simple peut être plus facile à gérer que deux composants nécessitant une coordination minutieuse
Couplage faible et interface commune
- Un système dans lequel on peut supprimer une partie sans réécrire les autres est généralement dit à couplage faible
- Le couplage faible correspond à un état où changer d’avis n’oblige pas à modifier trop de code
- Même coder une valeur en dur une seule fois, ou utiliser un flag de ligne de commande au lieu d’une variable, peut selon le cas relever du couplage faible
- Microsoft Windows atteint cet objectif en séparant API externes et API internes
- Les API externes sont liées au cycle de vie des programmes de bureau
- Les API internes sont liées au noyau sous-jacent
- Cacher les API permet de conserver de la flexibilité sans casser beaucoup de logiciels
- HTTP fournit aussi des exemples de couplage faible
- On peut placer un cache devant un serveur HTTP
- Déplacer des images vers un CDN et ne changer que les liens ne casse pas les navigateurs
- Les codes d’erreur HTTP attribuent des codes spécifiques à des problèmes courants, ce qui permet aux clients de traiter de nombreuses erreurs à la place du serveur
- La gestion des pannes doit être prise en compte lorsque l’on découpe le code en petits morceaux
Gestion des pannes et degré de couplage
- Erlang/OTP utilise une approche assez particulière de la gestion des pannes avec des arbres de supervision
- Dans un système Erlang, chaque processus est grosso modo démarré et surveillé par un supervisor
- Si un processus rencontre un problème, il s’arrête
- Lorsqu’un processus s’arrête, le supervisor le redémarre
- Le supervisor est lancé par un bootstrap process ; si le supervisor tombe en panne, le bootstrap process le redémarre
- L’idée centrale est qu’il est plus rapide d’échouer vite et de redémarrer que de gérer l’erreur
- Certaines pannes temporaires peuvent être contenues en éteignant puis rallumant
- La gestion et la récupération des erreurs devraient être assurées dans les couches externes de la base de code ; c’est ce qu’on appelle le principe de bout en bout
- Il est plus facile de gérer les échecs aux deux extrémités d’une connexion qu’au milieu, et même si on les traite à l’intérieur, une vérification de plus haut niveau reste nécessaire
- La gestion des erreurs est l’une des nombreuses façons de lier fortement un système
IMAP, systèmes de fichiers, SQL, middleware
- IMAP est pénible pour la gestion des erreurs, car presque chaque opération prend une forme exceptionnelle avec ses propres options et traitements
- Dans IMAP, une erreur peut apparaître au milieu des résultats d’une autre opération
- Au lieu d’un UUID, IMAP crée un jeton unique pour identifier chaque message, et ce jeton peut aussi changer au milieu des résultats d’une opération
- Beaucoup d’opérations IMAP ne sont pas atomiques
- Il a fallu plus de 25 ans pour disposer d’un moyen fiable de déplacer un e-mail d’un dossier à un autre
- Il existe aussi un encodage UTF-7 spécial et un encodage base64 propre à IMAP
- Les systèmes de fichiers et les bases de données constituent de meilleurs points de comparaison pour le stockage distant
- Un système de fichiers possède un ensemble fixe d’opérations et plusieurs objets
- SQL peut sembler une interface plus vaste qu’un système de fichiers, mais il suit le même schéma : plusieurs opérations sur des ensembles et plusieurs lignes
- Les bases de données ne sont pas toujours interchangeables, mais il est plus facile de trouver quelque chose qui fonctionne avec SQL qu’avec un langage de requête maison
- Finagle de Twitter utilise une API commune pour les services, ce qui permet d’ajouter facilement aux codes client et serveur la gestion des timeouts, les mécanismes de retry et les vérifications d’authentification
- Les bons exemples de couplage faible sont souvent aussi des exemples d’interface uniforme
- Une base de code saine n’a pas besoin d’être parfaitement modularisée, mais il doit y avoir suffisamment de distance entre les pièces mobiles
- Un code faiblement couplé n’est pas forcément facile à supprimer, mais il est beaucoup plus facile à remplacer et à modifier
Étape 7 : continuer à écrire du code
- Si l’on peut écrire du nouveau code sans toucher à l’ancien, il est beaucoup plus facile d’expérimenter de nouvelles idées
- L’essentiel n’est pas de savoir s’il s’agit de microservices ou d’un monolithe, mais de pouvoir placer une ou deux expérimentations au-dessus du système pendant que l’on découvre ce qu’il doit faire
- Les feature flags sont une manière de pouvoir changer d’avis plus tard
- Les feature flags permettent non seulement d’expérimenter des fonctionnalités, mais aussi de déployer des changements sans redéployer le logiciel
- Google Chrome a constaté que, dans un cycle de releases régulières, la partie la plus difficile était le temps nécessaire pour fusionner des branches de fonctionnalités de longue durée
- Pouvoir activer et désactiver du nouveau code sans recompilation permet de découper de grands changements en petits merges, sans affecter le code existant
- Lorsque de nouvelles fonctionnalités apparaissent plus tôt dans la même base de code, l’impact d’un développement fonctionnel long sur les autres parties devient plus visible
- Un feature flag n’est pas un simple interrupteur en ligne de commande : c’est une façon de séparer la release d’une fonctionnalité de la fusion de branches et du déploiement du code
- Quand déployer un nouveau logiciel peut prendre des heures, des jours ou des semaines, la capacité de changer d’avis à l’exécution devient plus importante
Un bon code est du code legacy qui ne gêne pas
- Ce qui compte plus que le fait d’itérer, c’est d’avoir une boucle de feedback
- Plutôt que de créer des modules pour les réutiliser, l’essentiel est d’isoler les composants pour permettre le changement
- Répondre au changement inclut non seulement le développement de nouvelles fonctionnalités, mais aussi la suppression d’anciennes fonctionnalités
- Écrire du code extensible revient à espérer que le choix initial sera encore le bon dans trois mois
- Le code supprimable part de l’hypothèse inverse
- Layering, isolation, interfaces communes et composition sont des moyens de créer du logiciel capable d’évoluer avec le temps, plus que du bon logiciel en soi
- Il n’est pas nécessaire de tout jeter, mais certaines parties devront être supprimées
- Un bon code n’est pas celui qui était juste dès le départ, mais du code legacy qui ne gêne pas
- Un bon code est un code facile à supprimer
1 commentaires
Avis sur Hacker News
Une formule que j’aime bien est la simplicité est robuste
C’est proche de la loi du changement continu de Lehman : moins un système est complexe, plus il est facile à faire évoluer
Plutôt que d’écrire du code extensible en prévision du futur, il vaut mieux, à mon avis, se préparer à l’avenir avec du code intuitif
Par exemple, n’abstraire que quand c’est vraiment nécessaire, tolérer une duplication simple, commencer par un monolithe, et privilégier d’abord la montée en charge verticale plutôt qu’horizontale
J’ai construit plusieurs systèmes de type 0→1, et c’était à chaque fois le schéma commun
https://en.m.wikipedia.org/wiki/Lehman%27s_laws_of_software_...
Ce n’est pas parce qu’on ne gère pas les cas limites que le code devient plus robuste, même s’il a l’air plus simple
Cela ouvre un débat sur ce qu’est la “simplicité” et sur la manière de l’appliquer à un système, ce qui est déjà une question suffisamment complexe pour que Rich Hickey s’y intéresse
À la place, “ce qui est bête est robuste” ou “ce qui est direct est robuste” exprimerait peut-être mieux l’intention
Il suffit d’écrire du code qui fait le travail nécessaire. N’inventez pas de faux problèmes de passage à l’échelle, ne fabriquez pas d’abstractions astucieuses juste pour avoir l’air intelligent, écrivez un monolithe, déployez-le sur une VM, et c’est exploitable immédiatement
Si un problème apparaît, vous le réglerez à ce moment-là, et si possible après que la trésorerie soit devenue positive
Pourquoi une startup “AirBnb pour chiens” avec 0 utilisateur s’inquiéterait-elle de C100K ? Quand AWS vous a convaincu de payer du serverless, était-ce dans votre intérêt, ou pour vous soutirer de l’argent ?
Articles liés :
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=24989351 - Nov 2020 (30 comments)
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=23914486 - July 2020 (109 comments)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=18761739 - Dec 2018 (2 comments)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=11093733 - Feb 2016 (133 comments)
Pour résumer brièvement l’erreur que j’ai faite quand j’étais jeune, je crois maintenant à l’inverse, c’est-à-dire à la conception en vue de la suppression
Avant, je pensais pouvoir créer une œuvre élégante capable d’anticiper toutes les situations et de satisfaire tous les besoins. Mais personne ne prédit aussi bien les besoins futurs
Un jour, ce que j’ai construit deviendra pour quelqu’un “ce truc idiot”, et peu importe à quel point j’en suis fier aujourd’hui, il sera légitime qu’ils cassent tout
Mieux vaut donc consacrer ses efforts à faire quelque chose qu’on puisse enlever facilement. Cela réduit souvent le couplage, mais l’important, c’est que ce n’est pas le même découplage que celui recherché par les jeunes développeurs enthousiastes qui veulent tout séparer dans un framework entièrement configurable par méta-paramétrage
Parfois, un couplage fort mais facile à comprendre est préférable
https://news.ycombinator.com/item?id=41219130
Par exemple, on voit apparaître des choses comme CommonExcelFileParser, CommonExcelFileParserUtilities, HasExcelParseStatus, ProductImportExcelParser, ProductImportExcelParserView, ProductImportExcelParserResultHandler, qui finissent par devenir la base du code autour
C’est un peu comme démarrer un projet frontend avec React ou Angular : migrer ensuite vers autre chose devient un travail de Sisyphe
En pratique, les gens finissent par construire toute une plateforme, et même quand certains choix poseront problème plus tard, le couplage rend le refactoring bien plus difficile qu’avec une base de code sous-abstraite
J’ai l’impression que les gens aiment davantage faire ça qu’appliquer KISS et YAGNI pour produire du code facile à supprimer, et je ne sais pas quoi faire dans ce cas
Les exigences métier changent et bougent sans cesse, donc il ne faut pas essayer de les anticiper : il faut écrire quelque chose de facile à remplacer ou à jeter
Les frameworks et les bibliothèques, c’est un peu différent. Ils doivent aussi s’adapter aux évolutions du monde, mais à un rythme bien plus modéré
Le plus gros problème, c’est quand des développeurs veulent encore créer un “framework” à l’intérieur d’une application métier qui utilise déjà un framework comme Rails ou Asp.Net
À moins d’écrire un noyau Linux, il ne faut pas écrire comme le noyau Linux
Le fait que cet article n’aborde absolument pas les tests et l’observabilité est assez étrange
Les tests ont eux aussi un coût de maintenance, mais ils réduisent le risque de casser quelque chose quand on supprime un élément
En outre, si un service est exposé à des appelants externes, il faut à la fois un mécanisme robuste pour marquer certains appels comme obsolètes puis les supprimer plus tard, et un moyen d’observer s’ils sont encore utilisés et par qui
J’ai récemment semi-automatisé pour la première fois la suppression de résolveurs GraphQL exposés, et comme nous avions déjà des métriques sur la fréquence d’utilisation de certains résolveurs, j’ai pu les analyser pour obtenir une liste de ceux qu’on ne pouvait pas supprimer
GraphQL dispose déjà d’une annotation
deprecated, mais notre service ne la traitait pas spécialementNous avons donc ajouté de l’observabilité pour signaler quand une fonction obsolète est appelée, puis, après l’avoir laissé tourner assez longtemps en production, nous avons pu supprimer en toute sécurité du code exposé à l’extérieur
Dès qu’on commence à rendre les choses trop complexes, tout devient un bazar interconnecté et les développeurs ne savent plus quel impact auront leurs modifications
Bien sûr, il existe mille façons de s’y prendre mal. On peut suivre des principes de “bonnes pratiques” absurdes, ou faire du “microservice” d’une manière où personne ne sait qui consomme quel service. Mais dans ce cas, on n’a pas construit quelque chose de facile à supprimer
La consommation externe en est un bon exemple. Il est raisonnable de donner aux consommateurs un avertissement correct avant l’arrêt d’un service, mais si on ne peut pas réellement le couper quand on le souhaite, alors ce n’est pas un système conçu pour être supprimé facilement
Si cette approche est la bonne, alors très bien. Il ne faut simplement pas s’attendre à ce que des tests et de l’observabilité suffisent à dire si quelque chose casse, car cela risque de mal fonctionner
Je ne suis pas contre les tests en eux-mêmes, mais je ne les trouve pas particulièrement excellents comme garde-fou pour indiquer si quelque chose a cassé dans une chaîne longue et complexe. Il est en pratique très difficile d’avoir une couverture de tests qui protège réellement
Si on supprime une partie du code, on peut aussi supprimer une partie des tests
On peut considérer que, même si l’article ne parle que du code, les effets liés aux tests sont implicitement inclus
Ce n’est pas parce que l’article ne parle pas des tests qu’il faut supposer qu’il dit de ne pas en écrire
En lisant ce passage, j’ai l’impression que le titre n’est pas toujours juste : un code facile à supprimer est souvent aussi un code facile à étendre
Parce qu’il est organisé en couches, modulaire, et qu’il isole les différentes parties au moyen d’abstractions comme des interfaces ou d’autres contrats de type
J’ai l’habitude de dire aux étudiants en physique computationnelle que le meilleur calcul est celui qu’on n’a pas besoin de faire
Personnellement, je sépare le code en deux parties : la logique métier et l’implémentation proprement dite
La logique métier peut par nature comporter des duplications, mais il ne faut pas trop dupliquer les détails techniques
L’implémentation elle-même peut être aussi sale qu’on veut, tant qu’elle ne contient pas directement la logique métier et qu’elle reste indépendante de l’application
Quand c’est fait ainsi, si l’on se rend compte que quelque chose est bancal et ne fonctionne pas correctement, on a alors la possibilité de jeter toute l’implémentation au lieu de bricoler des correctifs en retraçant de force la vraie spécification depuis l’implémentation
L’affirmation du premier paragraphe selon laquelle “le problème de la réutilisation du code est qu’elle empêche de changer d’avis plus tard” est clairement une erreur
De manière générale, c’est faux. Si on change d’avis et que le code a été copié-collé à dix endroits, il faut corriger les dix endroits
À l’inverse, si c’est dans une fonction, il suffit de le changer une seule fois. Et si l’on découvre qu’un des dix appels ne doit finalement pas changer, on peut alors faire un copier-coller à ce moment-là, ou généraliser davantage la fonction
Comme traverser la rue sans regarder, le copier-coller est presque toujours une mauvaise idée
Mais une mauvaise abstraction peut déboucher sur des mois entiers de remboursement de dette technique
Bien sûr, la réponse est “ne faites pas de mauvaises abstractions”, mais avec les équipes et l’évolution des exigences produit, on sait tous très bien comment ça se passe
J’ai un sous-module git qui contient des widgets UI communs, et maintenant il est presque impossible d’en modifier un seul, au point qu’il est plus simple de copier le composant dans le projet et de le modifier localement
C’est un problème. Le code partagé doit être réduit au minimum autant que possible, et le simple fait de partager rend déjà le changement plus difficile
Si tout est regroupé dans une seule fonction, la plupart des développeurs vont essayer de modifier cette fonction pour satisfaire les 10 cas, alors qu’elle n’aurait jamais dû être une seule fonction au départ
Corriger dix occurrences copiées-collées est bien plus facile que défaire un nœud mal ficelé qui, une fois serré, retient ensemble les morceaux du système
En apparence, cela semble contradictoire avec sa position sur ce sujet, mais en lisant plus attentivement, il utilise le copier-coller comme un signal indiquant quel code devrait être abstrait, et quels motifs sont réellement à suivre
C’est étrange de continuer à répéter toutes sortes de commandements sur le logiciel, ces principes presque religieux
Sur le papier, tout cela paraît excellent et semble relever du bon sens, mais même après 50 ans, le logiciel reste médiocre dans 90 % des cas
Et malgré cela, on continue à ressortir ces idées comme si c’étaient des intuitions géniales ou des balles d’argent
Il y a ici un excellent corollaire. Le mauvais code est bien plus difficile à supprimer, donc il reste plus longtemps