Les six commandements d'un bon code : écrivez un code qui résiste à l'épreuve du temps
Publié: 2022-03-11Les humains ne se sont attaqués à l'art et à la science de la programmation informatique que depuis environ un demi-siècle. Comparée à la plupart des arts et des sciences, l'informatique n'est à bien des égards qu'un enfant en bas âge, marchant dans les murs, trébuchant sur ses propres pieds et jetant parfois de la nourriture sur la table.
En raison de sa jeunesse relative, je ne crois pas que nous ayons encore un consensus sur ce qu'est une définition appropriée du « bon code », car cette définition continue d'évoluer. Certains diront qu'un "bon code" est un code avec une couverture de test de 100 %. D'autres diront qu'il est super rapide et qu'il a des performances exceptionnelles et qu'il fonctionnera de manière acceptable sur du matériel vieux de 10 ans.
Bien que ce soient tous des objectifs louables pour les développeurs de logiciels, je me risque cependant à ajouter un autre objectif au mélange : la maintenabilité. Plus précisément, un « bon code » est un code qui est facilement et facilement maintenable par une organisation (pas seulement par son auteur !) et qui vivra plus longtemps que le sprint dans lequel il a été écrit. carrière d'ingénieur dans de grandes et petites entreprises, aux États-Unis et à l'étranger, qui semblent correspondre à des logiciels « bons » maintenables.
Commandement #1 : Traitez votre code comme vous voulez que le code des autres vous traite
Je suis loin d'être la première personne à écrire que le public principal de votre code n'est pas le compilateur/ordinateur, mais celui qui doit ensuite lire, comprendre, maintenir et améliorer le code (ce qui ne sera pas nécessairement vous dans six mois ). Tout ingénieur digne de ce nom peut produire un code qui « fonctionne » ; ce qui distingue un excellent ingénieur, c'est qu'il peut écrire efficacement du code maintenable qui soutient une entreprise à long terme et qu'il a les compétences nécessaires pour résoudre les problèmes simplement et de manière claire et maintenable.
Dans n'importe quel langage de programmation, il est possible d'écrire du bon code ou du mauvais code. En supposant que nous jugeons un langage de programmation par la façon dont il facilite l'écriture de bon code (cela devrait au moins être l'un des principaux critères, de toute façon), tout langage de programmation peut être "bon" ou "mauvais" selon la façon dont il est utilisé (ou abusé ).
Python est un exemple de langage considéré par beaucoup comme "propre" et lisible. Le langage lui-même applique un certain niveau de discipline d'espace blanc et les API intégrées sont nombreuses et assez cohérentes. Cela dit, il est possible de créer des monstres indescriptibles. Par exemple, on peut définir une classe et définir/redéfinir/annuler la définition de n'importe quelle méthode sur cette classe pendant l'exécution (souvent appelée correction de singe). Cette technique conduit naturellement au mieux à une API incohérente et au pire à un monstre impossible à déboguer. On pourrait naïvement penser "bien sûr, mais personne ne fait ça !" Malheureusement, c'est le cas, et il ne faut pas longtemps pour parcourir pypi avant de tomber sur des bibliothèques substantielles (et populaires !) qui (ab)utilisent largement les correctifs de singe comme cœur de leurs API. J'ai récemment utilisé une bibliothèque réseau dont l'intégralité de l'API change en fonction de l'état du réseau d'un objet. Imaginez, par exemple, appeler client.connect() et obtenir parfois une erreur MethodDoesNotExist au lieu de HostNotFound ou NetworkUnavailable .
Commandement #2 : Un bon code est facile à lire et à comprendre, en partie et en totalité
Un bon code est facilement lu et compris, en partie et en totalité, par les autres (ainsi que par l'auteur à l'avenir, en essayant d'éviter le syndrome « Ai-je vraiment écrit cela ? » ).
Par "en partie", je veux dire que si j'ouvre un module ou une fonction dans le code, je devrais pouvoir comprendre ce qu'il fait sans avoir à lire également tout le reste de la base de code. Il doit être aussi intuitif et auto-documenté que possible.
Le code qui fait constamment référence à des détails infimes qui affectent le comportement d'autres parties (apparemment non pertinentes) de la base de code revient à lire un livre dans lequel vous devez faire référence aux notes de bas de page ou à une annexe à la fin de chaque phrase. Vous n'obtiendrez jamais la première page !
Quelques autres réflexions sur la lisibilité « locale » :
Un code bien encapsulé a tendance à être plus lisible, séparant les préoccupations à tous les niveaux.
Les noms comptent. Activez le système 2 de Thinking Fast and Slow dans lequel le cerveau forme des pensées et mettez une réflexion réelle et prudente dans les noms de variables et de méthodes. Les quelques secondes supplémentaires peuvent rapporter des dividendes importants. Une variable bien nommée peut rendre le code beaucoup plus intuitif, tandis qu'une variable mal nommée peut entraîner des faux-semblants et de la confusion.
L'intelligence est l'ennemie. Lorsque vous utilisez des techniques, des paradigmes ou des opérations fantaisistes (comme les compréhensions de liste ou les opérateurs ternaires), veillez à les utiliser de manière à rendre votre code plus lisible, et pas seulement plus court.
La cohérence est une bonne chose. La cohérence dans le style, à la fois en termes de placement des accolades mais aussi en termes d'opérations, améliore considérablement la lisibilité.
Séparation des préoccupations. Un projet donné gère un nombre incalculable d'hypothèses localement importantes à divers points de la base de code. Exposez chaque partie de la base de code au moins de ces problèmes que possible. Supposons que vous disposiez d'un système de gestion des personnes dans lequel un objet personne peut parfois avoir un nom de famille nul. Pour quelqu'un qui écrit du code dans une page qui affiche des objets de personne, cela pourrait être vraiment gênant ! Et à moins que vous ne mainteniez un manuel des "hypothèses maladroites et non évidentes de notre base de code" (je sais que ce n'est pas le cas), votre programmeur de page d'affichage ne saura pas que les noms de famille peuvent être nuls et va probablement écrire du code avec un pointeur nul exception dans celui-ci lorsque le cas nul du nom de famille apparaît. Au lieu de cela, gérez ces cas avec des API et des contrats bien pensés que différents éléments de votre base de code utilisent pour interagir les uns avec les autres.
Commandement #3 : Un bon code a une mise en page et une architecture bien pensées pour rendre l'état de gestion évident
L'État est l'ennemi. Pourquoi? Parce qu'il s'agit de la partie la plus complexe de toute application et qu'elle doit être traitée de manière délibérée et réfléchie. Les problèmes courants incluent les incohérences de la base de données, les mises à jour partielles de l'interface utilisateur où les nouvelles données ne sont pas reflétées partout, les opérations dans le désordre, ou tout simplement un code complexe avec des instructions if et des branches partout conduisant à un code difficile à lire et encore plus difficile à maintenir. Placer l'état sur un piédestal pour qu'il soit traité avec le plus grand soin, et être extrêmement cohérent et délibéré en ce qui concerne la manière dont l'état est accessible et modifié, simplifie considérablement votre base de code. Certains langages (Haskell par exemple) appliquent cela au niveau programmatique et syntaxique. Vous seriez étonné de voir à quel point la clarté de votre base de code peut s'améliorer si vous avez des bibliothèques de fonctions pures qui n'accèdent à aucun état externe, puis une petite surface de code avec état qui fait référence à la fonctionnalité pure extérieure.

Commandement #4 : Un bon code ne réinvente pas la roue, il repose sur les épaules de géants
Avant de réinventer potentiellement une roue, réfléchissez à la fréquence du problème que vous essayez de résoudre ou de la fonction que vous essayez d'exécuter. Quelqu'un a peut-être déjà mis en œuvre une solution que vous pouvez exploiter. Prenez le temps de réfléchir et de rechercher de telles options, si elles sont appropriées et disponibles.
Cela dit, un contre-argument tout à fait raisonnable est que les dépendances ne sont pas « gratuites » sans aucun inconvénient. En utilisant une bibliothèque tierce ou open source qui ajoute des fonctionnalités intéressantes, vous vous engagez envers cette bibliothèque et vous en devenez dépendant. C'est un gros engagement; s'il s'agit d'une bibliothèque géante et que vous n'avez besoin que d'une petite fonctionnalité, voulez-vous vraiment mettre à jour toute la bibliothèque si vous mettez à niveau, par exemple, vers Python 3.x ? Et de plus, si vous rencontrez un bogue ou souhaitez améliorer la fonctionnalité, soit vous dépendez de l'auteur (ou du fournisseur) pour fournir le correctif ou l'amélioration, soit, si c'est open source, vous vous retrouvez dans la position d'explorer un ( base de code potentiellement substantielle), vous n'êtes absolument pas familier avec la tentative de réparer ou de modifier une fonctionnalité obscure.
Certes, plus le code dont vous dépendez est bien utilisé, moins vous aurez à investir du temps vous-même dans la maintenance. L'essentiel est qu'il vaut la peine pour vous de faire vos propres recherches et de faire votre propre évaluation pour savoir s'il faut ou non inclure une technologie extérieure et combien de maintenance cette technologie particulière ajoutera à votre pile.
Vous trouverez ci-dessous quelques-uns des exemples les plus courants de choses que vous ne devriez probablement pas réinventer à l'ère moderne dans votre projet (à moins que ce SONT vos projets).
Bases de données
Déterminez le CAP dont vous avez besoin pour votre projet, puis choisissez la base de données avec les bonnes propriétés. La base de données ne signifie plus seulement MySQL, vous pouvez choisir parmi :
- SQL schématisé « traditionnel » : Postgres / MySQL / MariaDB / MemSQL / Amazon RDS, etc.
- Magasins de valeurs clés : Redis / Memcache / Riak
- NoSQL : MongoDB/Cassandre
- Bases de données hébergées : AWS RDS / DynamoDB / AppEngine Datastore
- Levage lourd : Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
- Des trucs fous : Erlang's Mnesia, les données de base d'iOS
Couches d'abstraction de données
Dans la plupart des cas, vous ne devriez pas écrire de requêtes brutes dans la base de données que vous avez choisie d'utiliser. Plus probablement qu'autrement, il existe une bibliothèque entre la base de données et le code de votre application, séparant les problèmes de gestion des sessions de base de données simultanées et les détails du schéma de votre code principal. À tout le moins, vous ne devriez jamais avoir de requêtes brutes ou SQL en ligne au milieu de votre code d'application. Enveloppez-le plutôt dans une fonction et centralisez toutes les fonctions dans un fichier appelé quelque chose de vraiment évident (par exemple, "queries.py"). Une ligne comme users = load_users() , par exemple, est infiniment plus facile à lire que users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”) . Ce type de centralisation permet également d'avoir beaucoup plus facilement un style cohérent dans vos requêtes et limite le nombre d'endroits où aller pour modifier les requêtes si le schéma change.
Autres bibliothèques et outils courants à envisager
- File d'attente ou services Pub/Sub. Faites votre choix parmi les fournisseurs AMQP, ZeroMQ, RabbitMQ, Amazon SQS
- Espace de rangement. Amazon S3, Google Cloud Storage
- Surveillance : Graphite/Graphite hébergé, AWS Cloud Watch, New Relic
- Collecte de journaux / Agrégation. Loggly, Splunk
Mise à l'échelle automatique
- Mise à l'échelle automatique. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Digital Ocean
Commandement #5 : Ne traversez pas les ruisseaux !
Il existe de nombreux bons modèles pour la conception de programmes, pub/sub, acteurs, MVC, etc. Choisissez celui que vous préférez et respectez-le. Différents types de logique traitant de différents types de données doivent être physiquement isolés dans la base de code (encore une fois, cette séparation des préoccupations et la réduction de la charge cognitive du futur lecteur). Le code qui met à jour votre interface utilisateur doit être physiquement distinct du code qui calcule ce qui se passe dans l'interface utilisateur, par exemple.
Commandement n° 6 : Lorsque c'est possible, laissez l'ordinateur faire le travail
Si le compilateur peut détecter des erreurs logiques dans votre code et empêcher un mauvais comportement, des bogues ou des plantages purs et simples, nous devons absolument en profiter. Bien sûr, certains langages ont des compilateurs qui rendent cela plus facile que d'autres. Haskell, par exemple, a un compilateur réputé strict qui fait que les programmeurs consacrent la plupart de leurs efforts à obtenir du code à compiler. Une fois compilé, "ça marche". Pour ceux d'entre vous qui n'ont jamais écrit dans un langage fonctionnel fortement typé, cela peut sembler ridicule ou impossible, mais ne me croyez pas sur parole. Sérieusement, cliquez sur certains de ces liens, il est tout à fait possible de vivre dans un monde sans erreurs d'exécution. Et c'est vraiment magique.
Certes, tous les langages ne disposent pas d'un compilateur ou d'une syntaxe qui se prête à beaucoup (ou dans certains cas à aucun !) vérification au moment de la compilation. Pour ceux qui ne le font pas, prenez quelques minutes pour rechercher les contrôles de rigueur facultatifs que vous pouvez activer dans votre projet et évaluez s'ils ont un sens pour vous. Une liste courte et non exhaustive de certains langages courants que j'ai utilisés récemment pour les langages avec des temps d'exécution indulgents comprend :
- Python : pylint, pyflakes, avertissements, avertissements dans emacs
- Rubis : avertissements
- JavaScript : jslint
Conclusion
Il ne s'agit en aucun cas d'une liste exhaustive ou parfaite des commandements pour produire du « bon » code (c'est-à-dire facilement maintenable). Cela dit, si chaque base de code que j'ai eu à acquérir à l'avenir suivait ne serait-ce que la moitié des concepts de cette liste, j'aurai beaucoup moins de cheveux gris et je pourrais même ajouter cinq années supplémentaires à la fin de ma vie. Et je trouverai certainement le travail plus agréable et moins stressant.
