I. Introduction▲
Lire le code d’autrui peut être déroutant. Des heures peuvent s’écouler pour résoudre des problèmes qui auraient pu être corrigés en quelques minutes. Dans cet article, Artur Śmiarowski souhaite partager des conseils sur comment écrire un code qui sera plus facile à comprendre et à maintenir.
Avant de commencer, veuillez noter qu’il ne s’agit pas d’un guide pour écrire du « code propre ». Les gens ont tendance à comprendre différentes choses avec ce terme, certains le préféreront facilement extensible et générique. D’autres préféreront abstraire l’implémentation et juste assurer la configuration et d'autres, qui voudront juste voir un code subjectivement beau. Ce guide se concentre sur la visibilité du code. Par cela j’entends un bout de code qui communique les informations nécessaires aux autres développeurs, aussi efficacement que possible.
Vous trouverez ci-dessous 23 principes qui vous aideront à écrire du code plus lisible. Cet article est long, n’hésitez pas à aller directement aux parties qui vous intéressent.
II. Soyez conscient de votre problème avant de créer la solution▲
Que vous corrigiez un bogue, ajoutiez une nouvelle fonctionnalité ou conceviez une application, vous résolvez un problème pour une autre personne. Idéalement, vous voudrez le faire en laissant le minimum d’anomalies derrière vous. Vous devriez être clair sur les problèmes que vous résolvez avec vos choix de « design pattern » (patron de conception), remaniements, dépendances externes, bases de données et les toutes autres choses sur lesquelles vous passez du temps précieux.
Votre bout de code est un problème potentiel. Même le plus beau code l’est. Le seul moment où un bout de code n’est plus un problème est lorsqu’un projet est fini et mort, ou n’est plus supporté. Pourquoi ? Parce que quelqu’un d’autre va devoir le lire durant sa vie, le comprendre, le corriger, l’étendre, voire même supprimer entièrement la fonctionnalité qu’il apporte.
Maintenir le code de base prend beaucoup de temps et peu de développeurs aiment le faire, car cela manque de créativité. Écrivez votre code le plus simplement de façon à ce qu’un développeur junior puisse le corriger quand cela est nécessaire et vous serez libre de gérer des problèmes plus importants.
La perte de temps est un problème. Une solution parfaite pour votre tâche peut être disponible, mais il est parfois difficile pour un développeur de la voir. Il y a des tâches pour lesquelles la meilleure solution est de convaincre le client que ce qu’il recherche n’est pas forcément ce dont il a besoin. Cela nécessite une compréhension plus profonde de l’application et de son objectif. Votre client souhaite peut être tout un module qui finira par devenir des milliers de lignes de code supplémentaires, alors qu’il a juste besoin de personnaliser davantage ses options existantes. Il se peut que vous ne deviez que légèrement modifier le code existant, économisant ainsi du temps et de l’argent.
Il y a d’autres types de problèmes. Disons que vous deviez implémenter une liste d’enregistrements avec des filtres. Les données sont stockées dans une base de données, mais le lien entre les différents enregistrements sont complexes. Après avoir analysé la manière dont le client veut que les données soient filtrées, vous découvrez que la complexité de la base de données va nécessiter de passer 20 heures à construire des requêtes SQL complexes, avec des jointures multiples et des requêtes internes. Pourquoi ne pas expliquer qu’il existe une solution différente qui ne prendra qu’une heure, mais qui ne couvrira pas toute la fonctionnalité ? Il se peut que cette fonctionnalité supplémentaire ne mérite pas cet effort, ce qui se traduira par des économies.
III. Choisissez le bon outil de travail▲
Ce langage si spécial, ce framework que vous adorez tant, ou un nouveau moteur de base de données, peuvent s’avérer ne pas être le bon outil pour le problème que vous rencontrez. Dans un projet sérieux, ne choisissez pas des outils dont vous avez entendu parler comme étant fabuleux pour tout. Cela vous conduira au désastre. Si vos données ont besoin de relations, choisir MongoDB uniquement pour apprendre, finira mal. Vous savez que vous pouvez le faire, mais souvent vous aurez besoin d’une solution de contournement qui générera du code supplémentaire et apportera des solutions qui ne sont pas optimales. Bien sûr, vous pouvez enfoncer un clou avec une planche de bois, mais une recherche google rapide vous renverrait vers un marteau. Peut-être que depuis votre dernière recherche il existe une intelligence artificielle qui peut le faire automatiquement pour vous.
IV. La simplicité est reine▲
Vous avez peut être déjà entendu la phrase « une optimisation prématurée est la source de tous les maux ». Elle détient une vérité incomplète. Vous devriez préférer des solutions simples à moins que vous ne soyez sûr que ça ne fonctionnera pas, mais il ne faut pas simplement le croire, il faut l’avoir déjà vérifié ou préalablement calculé et en être certain. Choisir une solution plus complexe pour quelque raison que ce soit, la vitesse d’exécution, le manque de mémoire, le manque de dépendances ou toute autre raison peut avoir un impact important sur la lisibilité du code. Ne compliquez pas les choses sauf si vous avez à le faire. L’exception à cela serait de connaître une solution plus efficace et de savoir que son implémentation n’impactera pas la lisibilité et votre planning.
De la même manière, vous n’avez pas besoin d’utiliser toutes les nouvelles fonctionnalités de votre langage si cela ne vous profite pas à vous et à votre équipe. Nouveau, ne signifie pas meilleur. Si vous n’êtes pas sûr, recommencez au début et reconsidérez le problème que vous essayez de résoudre avant d’effectuer des modifications. Si vous avez besoin de l’index d’une boucle, ce n’est pas parce qu’il existe différentes nouvelles manières d’en écrire en Javascript que la syntaxe « for » est obsolète.
V. Vos fonctions, classes et composants doivent avoir un objectif bien défini▲
Connaissez-vous les principes « SOLIDSOLID » ? Je les ai trouvés judicieux pour concevoir des bibliothèques génériques, mais même si je les ai utilisés quelques fois et que j’en ai vu des implémentations dans des projets, je pense que les règles sont un peu trop confuses et compliquées.
Décomposez votre code en fonctions pour que chacune d’entre elles ne fasse qu’une chose. Par exemple, considérons la façon dont nous implémenterions un bouton. Ce pourrait être une classe qui regroupe toutes les fonctionnalités d'un bouton. Vous pourriez implémenter le bouton avec une fonction pour l’afficher sur l’écran, une autre pour le surligner quand il est survolé par la souris, puis une autre pour le clic du bouton et encore une autre pour l'animer lors du clic. Vous pouvez le décomposer encore plus. Si vous avez besoin de calculer le rectangle de la position du bouton basé sur la résolution d’écran, ne le faites pas dans la fonction d’affichage. Implémentez ce calcul dans une classe différente, car elle sera utilisable par d’autres éléments de l’IHM et utilisez-la pour afficher le bouton.
C’est une chose simple à suivre, dès que vous pensez « ceci n’a rien à faire ici », vous pouvez le déplacer dans une autre fonction, en fournissant davantage d’informations aux collègues en encapsulant un bloc de code avec un nom de fonction et des commentaires.
Examinez les exemples ci-dessous qui font la même chose, lequel informe le plus vite de ce qu’il fait ?
// C++
if
(currentDistance <
radius2) {
// C'est la vue du joueur
if
(!
isLight) {
// Si l'éclairage de la tuile est d'environ 30% (alors la vue dans l'obscurité est pire) ou la distance du joueur est de 1, la tuile devrait être visible.
if
(hasInfravision ||
map.getLight(mapPosition) >
0.29
f ||
ASEngine::vmath::
distance(center, mapPosition) ==
1
) {
map.toggleVisible(true
, mapPosition);
}
}
// Ceci est pour le calcul de l'éclairage
else
{
ASEngine::
ivec3 region =
World::
inst().map.currentPosition;
ASEngine::
ivec2 pos =
mapPosition;
if
(mapPosition.x >
63
) {
pos.x -=
64
;
region.x +=
1
;
}
else
if
(mapPosition.x <
0
) {
pos.x +=
64
;
region.x -=
1
;
}
if
(mapPosition.y >
63
) {
pos.y -=
64
;
region.y +=
1
;
}
else
if
(mapPosition.y <
0
) {
pos.y +=
64
;
region.y -=
1
;
}
map.changeLight(pos, region, 1.0
f -
static_cast
(currentDistance) /
static_cast
(radius2));
}
}
// C++
if
(currentDistance <
radius2) {
// C'est la vue du joueur
if
(!
isLight) {
this
->
markVisibleTile(hasInfravision, map, center, mapPosition);
}
// Ceci est pour le calcul de l'éclairage
else
{
ASEngine::
ivec3 region =
World::
inst().map.currentPosition;
ASEngine::
ivec2 pos =
map.getRelativePosition(mapPosition, region);
map.changeLight(pos, region, 1.0
f -
static_cast
(currentDistance) /
static_cast
(radius2));
}
}
VI. Nommer est difficile, mais c’est important▲
Les noms des variables et des fonctions doivent être distincts et fournir une idée générale sur leur usage. Il est important que les noms décrivent l’utilité pour votre équipe et qu’ils soient conformes aux conventions retenues par le projet. Même si vous n’êtes pas d’accord avec elles. Si chaque requête pour rechercher un enregistrement dans une base de données commence avec le mot « find », comme par exemple « findUser », alors votre équipe pourrait être désorientée si vous nommez votre fonction « getUserProfile », car vous y êtes habitué. Essayez de regrouper les noms quand cela est possible. Par exemple, si vous avez plusieurs classes pour la validation des entrées, rajoutez « Validator » comme suffixe du nom peut fournir rapidement une information sur l’objectif de la classe.
Choisissez et respectez un type de casse conforme aux standards. Il peut être déroutant de se retrouver avec une casse de chameau (camelCase), snake_case, kebab-case (les mots sont en minuscule et sont liés par des tirets) et beer-case utilisés dans différents fichiers du même projet.
VII. Ne dupliquez pas le code▲
Nous avons déjà établi que le code est un problème. Du coup, pourquoi dupliquer vos problèmes pour gagner quelques minutes ? Cela n’a pas de sens. Vous pensez peut-être que vous aller résoudre quelque chose rapidement, simplement par un copier-coller. Mais si vous avez à copier plus de deux lignes de code, dites-vous que vous pourriez rater une occasion de trouver une meilleure solution. Peut-être une fonction générique ou une boucle ?
VIII. Supprimer le code inutilisé, ne le laissez pas en commentaire▲
Le code en commentaire est déroutant. A-t-il été retiré temporairement ? Est-il important ? Quand a t'il été commenté ? C’est inutile, faites-le disparaître. Je comprends que vous soyez hésitant à supprimer ce code, car les choses peuvent mal se passer et vous voulez simplement décommenter. Vous pourriez même y être très attaché suite à tout le temps et l’énergie que vous y avez mis. Ou peut-être pensez-vous que cela pourrait être nécessaire « bientôt ». La solution à tous ces problèmes est un logiciel de gestion de versions. Utilisez l’historique de GIT pour retrouver le code si jamais vous en avez besoin. Et nettoyez après vous !
IX. Les valeurs constantes doivent être en constantes statiques ou énumération▲
Utilisez-vous des chaînes de caractères ou des entiers pour définir les types des objets ? Par exemple, un utilisateur peut avoir un rôle « administrateur » ou « invité ». Comment allez vous vérifier s'il a le rôle « administrateur » ?
if ($user
->
role ==
"
admin
"
) {
// L'utilisateur est un administrateur
}
Ceci n’est pas génial. Premièrement, si le nom « admin » change, vous aurez à le modifier dans toute l’application. Vous pourriez dire que cela arrive rarement et que les EDI modernes facilitent le remplacement. C’est vrai. L’autre raison est l’absence de saisie semi-automatique et que des erreurs de syntaxe peuvent survenir. Elles peuvent être assez difficile à déboguer.
En définissant des constantes globales ou des énumérations, selon le langage utilisé, vous pouvez profiter de la saisie semi-automatique et modifier une valeur à un seul endroit, si jamais vous en avez besoin. Vous n’avez même pas à retenir quel type de valeur est caché derrière la constante, laissez seulement faire l'EDI et la magie de la saisie semi-automatique.
Il ne s’agit pas simplement du type de vos objets. En PHP, vous pouvez définir des tableaux avec des chaînes de caractères en tant que noms de champs. Avec les structures complexes, il peut être difficile de ne pas faire d’erreur de type. Et pour cette raison, il est préférable d’utiliser des objets à la place. Essayez d’éviter de coder avec des chaînes de caractères et vous ferez moins d’erreur de type et irez plus vite grâce à la saisie semi-automatique.
// PHP
const ROLE_ADMIN =
"
admin
"
;
if ($user
->
role ==
ROLE_ADMIN) {
// l'utilisateur est un administrateur
}
// C++
enum
class
Role {
GUEST, ADMIN }
; // Il est possible de mapper ces énumérations vers des chaînes de caractères, mais ce n'est pas nécessaire.
if
(user.role ==
Role.ADMIN) {
// l'utilisateur est un administrateur
}
X. Préférez les fonctions natives aux solutions personnalisées▲
Si votre langage ou le framework que vous avez retenu pour votre projet fournit une solution à votre problème, utilisez-la. Chacun peut rapidement chercher sur Internet ce qu’une fonction fait, même si elle n’est pas souvent utilisée. Cela prendra probablement plus de temps pour arriver à comprendre votre solution personnalisée. Si vous trouvez un bout de code qui fait la même chose dans une fonction interne, refactorisez-le rapidement, ne le laissez pas en l'état. Le code supprimé n’est plus un problème, il est donc important de le supprimer !
XI. Utilisez les recommandations spécifiques du langage▲
Si vous écrivez en PHP, vous devriez connaître les PSRs. Pour le Javascript, il existe une recommandation décente de Airbnb. Pour le C++, il existe une recommandation de Google ou les instructions de base de Bjarne Stroustrup, le créateur du C++. Les autres langages doivent aussi avoir leurs propres recommandations pour la qualité du code et vous pouvez même fournir vos propres standards à votre équipe. L’importance est de renforcer l’utilisation des recommandationsUtilisez des outils d’analyse de code statique choisies pour le projet, de sorte qu’il existe une vision unifiée de comment ça doit être développé. Cela évite de nombreux problèmes provenant de personnes différentes ayant des expériences uniques et faisant ce à quoi elles sont habituées.
XII. Evitez de créer des blocs de code imbriqués les uns dans les autres▲
Comparez ces deux blocs de code :
void
ProgressEffects::
progressPoison(Entity entity,
std::
shared_ptr<
Effects>
effects) {
float
currentTime =
DayNightCycle::
inst().getCurrentTime();
if
(effects->
lastPoisonTick >
0.0
f
&&
currentTime >
effects->
lastPoisonTick +
1.0
f) {
if
(effects->
poison.second >
currentTime) {
std::
shared_ptr <
Equipment >
eq =
nullptr
;
int
poisonResitance =
0
;
if
(this
->
manager.entityHasComponent(entity, ComponentType::
EQUIPMENT)) {
eq =
this
->
manager.getComponent <
Equipment >
(entity);
for
(size_t i =
0
; i <
EQUIP_SLOT_NUM; i++
) {
if
(eq->
wearing[i] !=
invalidEntity
&&
this
->
manager.entityHasComponent(eq->
wearing[i],
ComponentType::
ARMOR)) {
std::
shared_ptr<
Armor>
armor =
this
->
manager.getComponent <
Armor>
(eq->
wearing[i]);
poisonResitance +=
armor->
poison;
}
}
}
int
damage =
effects->
poison.first -
poisonResitance;
if
(damage <
1
)
damage =
1
;
std::
shared_ptr<
Health>
health =
this
->
manager.getComponent <
Health>
(entity);
health->
health -=
damage;
}
else
{
effects->
poison.second =
-
1.0
f;
}
}
}
void
ProgressEffects::
progressPoison(Entity entity, std::
shared_ptr<
Effects>
effects)
{
float
currentTime =
DayNightCycle::
inst().getCurrentTime();
if
(effects->
lastPoisonTick <
0.0
f ||
currentTime <
effects->
lastPoisonTick +
1.0
f) return
;
if
(effects->
poison.second <=
currentTime) {
effects->
poison.second =
-
1.0
f;
return
;
}
int
poisonResitance =
this
->
calculatePoisonResistance(entity);
int
damage =
effects->
poison.first -
poisonResitance;
if
(damage <
1
) damage =
1
;
std::
shared_ptr<
Health>
health =
this
->
manager.getComponent<
Health>
(entity);
health->
health -=
damage;
}
Le deuxième est plus facile à lire, n’est ce pas ? Si une telle solution est possible, évitez d’imbriquer les blocs conditionnels et les boucles l’un dans l’autre. Une astuce consiste à inverser l’instruction « if » et de revenir à la fonction appelante avant de passer au code suivant, comme dans l’exemple ci-dessus.
XIII. Ce n’est pas le nombre de lignes qui compte▲
Nous disons souvent qu’un bout de code qui prend le moins de ligne pour accomplir la tâche est meilleur. Certains d’entre nous deviennent même obsédés sur le nombre de lignes de code que nous ajoutons ou retirons, mesurant notre productivité selon ce nombre. Nous faisons ça pour la simplification, mais ce n’est pas une règle qui devrait être suivie sans tenir compte de la lisibilité. Vous pouvez réduire le tout à une seule ligne de code, mais il y a des chances que ce soit plus difficile à comprendre qu’en séparant en quelques lignes simples avec une seule commande par ligne.
Des langages offrent la possibilité d’écrire des expressions conditionnelles courtes, comme :
$variable
==
$x
?
$y
:
$z
;
// if ($variable == x) { $result = $y; } else { $result = $z; }
Ce peut être un bon choix, mais ça peut également être exagéré :
$variable
==
$x
?
($x
==
$y
?
array_merge($x
,
$y
,
$z
) :
$x
) :
$y
;
// Pardon ???
Ce doit être plus facile à comprendre après développement :
$result
=
$y
;
if ($variable
==
$x
&&
$x
==
$y
) $result
=
array_merge($x
,
$y
,
$z
);
else if ($variable
==
$x
) $result
=
$x
;
Ces trois lignes prennent plus d’espace sur l’écran, mais cela prend moins de temps à analyser ce qui est fait avec les données.
XIV. Apprenez les modèles de conception et quand ne pas les utiliser▲
Il existe différents modèles de conception qui sont souvent choisis pour résoudre des problèmes de développement. Bien qu'ils puissent résoudre des problèmes spécifiques, il faut garder en tête que leur utilité peut être affectée par différents facteurs comme la taille du projet, le nombre de personnes qui y travaillent, les contraintes de temps (coût) ou la complexité requise de la solution. Certains modèles ont été nommés anti-modèles, comme le Singleton, car même s’ils apportent des solutions, ils introduisent aussi des problèmes dans certains cas.
Soyez sûr de comprendre le coût de l’implémentation en terme de complexité introduite avant de choisir un modèle de conception pour votre solution. Vous n’avez peut-être pas besoin d’un modèle "Observer" pour communiquer entre composants dans un système simple. Quelques booléens permettront peut-être une solution facile à suivre. Il est plus justifié de passer du temps à implémenter un modèle de conception sélectionné dans une application plus grande, plus complexe.
XV. Séparez vos classes en gestionnaire de données et en manipulateur de données▲
Un gestionnaire de classe de données est une classe qui conserve des données dans sa structure interne. Elle permet d’accéder aux données via des accesseurs (getters) et des mutateurs (setters) selon les besoins, mais ne manipule pas les données sauf si elles sont modifiées lorsque vous les conservez dans le système ou si elles doivent toujours être modifiées lors de l'accès.
Un très bon exemple se trouve dans le modèle architectural Entity Component System, où les composants ne contiennent que les données et les systèmes les traitent, et les manipulent. Un autre cas d’utilisation serait le patron de conception « Entrepôt » implémenté pour la communication avec une base de données externe, où une classe « Model » représente les données de la base avec une structure spécifique de langage et la classe « Entrepôt » synchronise les données avec une base, soit en répercutant les modifications sur le modèle, soit en récupérant les données.
Cette séparation rend plus facile la compréhension des différentes parties de votre application. Reprenons l’exemple ci-dessus de l’entrepôt. Si vous souhaitez afficher une liste de données contenues dans une collection de « Models », avez-vous besoin de savoir d’où proviennent les données ? Avez-vous besoin de savoir comment les données sont stockées dans la base de données et comment cela a besoin d’être lié à des structures spécifiques à un langage ? Non. Vous récupérez les « Models » au travers des méthodes existantes et vous vous concentrez uniquement sur votre tâche, afficher les données.
Qu’en est-il de l’exemple Entity Component System ? Si vous devez implémenter des systèmes qui gèrent l’utilisation d’une compétence, jouer des animations, du son, infliger des dégâts, etc. Vous n’avez pas besoin de connaître la façon dont la compétence a été actionnée. Cela n’a pas d’importance si un script d’IA a initié la compétence sous certaines conditions ou si un joueur a utilisé une touche de raccourci pour l’activer. La seule chose que vous devez savoir, c'est que les données du « Component » ont changé, indiquant quelle compétence est à gérer.
XVI. Corrigez les problèmes à leur racine▲
Vous avez besoin d’implémenter une nouvelle fonctionnalité, en l’ajoutant à du code existant. Dans ce code vous rencontrez un problème. La structure d’entrée de la fonction ne fonctionne pas correctement avec vos besoins. Du coup vous devez écrire un peu plus de code pour réorganiser les données et en extraire davantage avant d’implémenter votre solution.
Avant de le faire, tentez de revenir quelques étapes en arrière dans votre code. D’où viennent ces données et comment sont-elles utilisées ? Vous pouvez peut-être les récupérer plus facilement d’une source externe ou les modifier dès leur acquisition ? En corrigeant ce problème à sa racine, vous pourrez peut-être résoudre le même problème à plusieurs endroits et pour des fonctionnalités ou modifications futures. Essayez toujours de simplifier la manière dont vous conservez vos données pour en faciliter l’accès dès que vous les recevez. Ceci est particulièrement important lorsque les données proviennent d’une source extérieure. Si vous avez besoin de données d’utilisateurs de l’application ou d’API externes, vous devriez éliminer les éléments inutiles et réorganiser le reste immédiatement.
XVII. Les pièges cachés de l’abstraction▲
Pourquoi écrivons-nous des solutions générales, abstraites à nos problèmes? Pour étendre facilement nos applications, faciliter l’adaptation aux nouveaux besoins et réutiliser nos bouts de codes, de telle sorte que nous n’ayons plus besoin de les réécrire.
Il y a souvent un coût important à l’abstraction en terme de lisibilité. Le plus haut niveau d’abstraction est lorsque tout est résolu alors que l’implémentation est cachée. On vous donne la capacité de configurer la façon de traiter vos données entrantes, mais vous n’avez aucun contrôle sur les détails, comme par exemple comment elles seront stockées dans la base de données, avec quelle efficacité elles seront traitées, quelles informations seront enregistrées, etc. L’avantage de cette solution est que si une nouvelle source de données doit être traitée de la même manière que la source actuelle, il est facile de la configurer avec la bibliothèque et de sélectionner l’endroit de stockage. Vous négociez essentiellement le contrôle de la vitesse d'implémentation.
Quand quelque chose se passe mal et qu’il ne s’agit pas d’un problème correctement documenté, quelqu’un aura beaucoup de difficulté à comprendre toutes les idées d’usage général qui tente de résoudre beaucoup plus que nécessaire. Si nous pouvons le permettre, nous devrions réellement éviter de cacher les détails de l’implémentation. Conserver le contrôle sur la base de données permet plus de flexibilité. N’écrivez pas une solution générale pour un simple problème uniquement parce que vous pensez que cela pourrait être étendu dans le futur. C’est rarement le cas et ça peut être réécrit lorsque nécessaire.
Prenons un exemple : si vous créez une classe, en 10-15 lignes de codes lisibles, qui importe des données depuis un fichier CSV et les stocke dans une base de données, pourquoi s’embêter à faire deux classes et généraliser la solution afin qu’elle puisse éventuellement être étendue pour importer du XLS ou du XML dans le futur, quand vous n’avez même pas la moindre idée si cela sera nécessaire pour votre application ? Pourquoi traîner une bibliothèque externe de 5000 lignes de code dont vous n’avez pas besoin pour résoudre ce problème ?
Il est rarement nécessaire de rendre générique l'emplacement de stockage de vos données. Combien de fois dans votre carrière avez-vous changé de moteur de base de données ? Au cours de ces dix dernières années, j’ai rencontré une seule fois un problème qui a été résolu de cette manière. Créer des solutions abstraites est coûteux et très souvent inutile à moins que ce ne soit pour une bibliothèque qui doit gérer une grande variété de projets à la fois.
En revanche, lorsque vous êtes certain de devoir permettre l’import depuis des fichiers XLS et CSV, alors la solution générale devrait être un choix parfaitement viable. Il est aussi envisageable d’écrire une solution générale par la suite, lorsque les exigences de votre application changent. Il sera beaucoup plus facile pour une personne de disposer d’une solution simple et claire lorsqu’elle aura besoin de la remplacer.
XVIII. Les règles du monde ne sont pas les règles de votre application▲
J’ai eu un argument intéressant concernant la modélisation « du monde réel » lors de l’implémentation du paradigme POO dans une application. Disons que nous devons traiter des données volumineuses pour un système publicitaire. Il y a deux types de messages de "log". Le premier, qui contient des données concernant l’émission d’une publicité. Le deuxième, qui contient les mêmes données que l’émission et quelques champs supplémentaires, informe des clics des personnes sur la publicité.
Dans le monde réel, nous pourrions envisager les deux actions, visionner et cliquer sur une publicité comme séparées mais similaire. Ainsi en modélisant le monde réel, nous pourrions créer une classe « Log » de base que nous étendrions aux classes « ClickLog » et « EmissionLog », comme ci-dessous :
struct
Log {
int
x;
int
y;
int
z;
}
struct
EmissionLog : public
Log {}
struct
ClickLog : public
Log {
float
q;
}
L’exemple ci-dessus montre très bien comment le système fonctionne dans le monde réel. Émettre une annonce est complètement différent de quelqu’un qui clique dessus. Cependant, un tel choix ne donne pas une information importante. Dans notre application, tout ce qui peut traiter un « log » d’émission peut fonctionner sur les clics. Nous pouvons utiliser les mêmes classes pour traiter les deux, mais seuls certains processeurs de « log » de clics ne peuvent pas fonctionner sur les « logs » d’émission, en raison de la différence des données.
Dans notre application, différente du monde réel, notre classe « ClickLog » est une extension de « EmissionLog ». Elles peuvent être traitées de la même manière, en utilisant les classes qui fonctionnent sur « EmissionLog ». En étendant la classe « ClickLog » d’après « EmissionLog », vous informez vos collègues que tout ce qui peut arriver à une émission peut arriver à des clics, sans qu'ils aient besoin de connaître tous les processeurs de « log » possibles dans l'application.
struct
EmissionLog {
int
x;
int
y;
int
z;
}
struct
ClickLog : public
EmissionLog {
float
q;
}
XIX. Typez vos variables si vous le pouvez, même si vous n’avez pas à le faire▲
Vous pouvez passer cette règle seulement si vous programmez dans des langages statiquement typés. Dans les langages à typage dynamique, comme PHP ou Javascript, il peut être très difficile de comprendre ce qu’un bout de code est supposé faire sans voir le contenu des variables. Pour la même raison, le code peut être très imprévisible quand une seule variable peut être un objet, un tableau ou null, en fonction de certaines conditions. Autorisez le moins possible de types de variables dans les paramètres de votre fonction. Des solutions sont disponibles. PHP peut avoir des arguments et des retours typés depuis la version 7. Et vous pouvez utiliser Typescript à la place du Javascript. Cela aide à la lisibilité du code et évite les erreurs stupides.
Si vous n’en avez pas besoin, ne permettez pas non plus l’utilisation de « Null ». « Null » est une abomination. Son existence doit être explicitement vérifiée pour éviter les erreurs fatales nécessitant du code inutile. Les choses sont encore plus terribles en Javascript avec son « null » et « undefined ». Marquez les variables pouvant être nulles pour informer vos pairs :
XX. Écrivez des tests▲
Au fil des années et en évitant l’épuisement professionnel, nous progressons à un point tel que nous pouvons cartographier les caractéristiques les plus complexes dans notre esprit et les implémenter sans vérifier si notre code fonctionne jusqu’à la mise en œuvre complète du premier brouillon. À ce stade, cela peut sembler une perte de temps d’écrire en cycle TDD, car il est un peu plus lent de vérifier chaque chose avant de l’écrire. C’est une bonne pratique d’écrire des tests d’intégrations qui permettent que la fonctionnalité fonctionne comme attendu, simplement parce que vous allez probablement laisser quelques petites erreurs et vous pourrez vérifier tout cela en quelques millisecondes.
Si vous n’êtes pas encore familiarisé avec votre langage ou votre bibliothèque et que vous essayez différentes approches pour résoudre votre problème, vous pouvez tirer un avantage considérable de la rédaction de tests. Cela encourage le fractionnement du travail en morceaux plus gérables. Les tests d’intégration expliquent rapidement quels types de problèmes le code résout, ce qui peut fournir des informations plus rapidement qu'une implémentation générique. Un simple « cette entrée entraîne cette sortie » peut accélérer le processus de compréhension de l’application.
XXI. Utilisez des outils d’analyse de code statique▲
Il existe de nombreux outils open-source d’analyse de code statique. Une grande partie est également fournie en temps réel par des EDI IDE à fonctionnalités avancées. Ils aident à garder vos projets sur la bonne voie. Vous pouvez en automatiser certains dans vos pipelines de stockage pour qu'ils soient exécutés sur chaque « commit » dans un environnement « docker ».
Choix solides pour PHP :
- Détecteur de copier/coller ;
- PHP Mess Detector – vérifie les bogues potentiels et la complexité ;
- PHP Code Sniffer – vérifie les standards de développement ;
- PHPMetrics – outil d’analyse statique avec tableau de bord et graphiques.
Javascript :
- JsHint / JsLint – recherchent les erreurs et les problèmes potentiels, peuvent être intégrés à un EDI pour analyse en temps réel ;
- Plato – outil de visualisation de code source et de complexité.
C++ :
- Cppcheck – détecte les bogues et les comportements indéfinis ;
- OClint – augmente la qualité du code.
Outil supportant différents langages :
- Pmd – détecteur de désordre.
XXII. Relecture par des pairs▲
La relecture de code se fait simplement par un autre développeur qui examine votre code pour trouver des erreurs et contribuer à l’amélioration de la qualité des logiciels. Même s'ils peuvent contribuer à la qualité globale de l'application et permettre un flux de connaissances au sein de l'équipe, ils ne sont utiles que si tout le monde est ouvert à la critique constructive. Parfois les personnes qui relisent imposent leur vision et leur expérience et n'acceptent pas un point de vue différent, ce qui peut être également difficile à accepter.
Il peut être difficile de réussir en fonction de l’environnement de l’équipe, mais les gains peuvent être incroyables. L'application la plus grande et la plus propre à laquelle j'ai participé au développement a été réalisée avec des revues de code très approfondies.
XXIII. Les commentaires▲
Vous avez peut-être déjà noté que j’aime conserver des règles de codage simples, de façon à ce qu’elles soient faciles à suivre par toute l’équipe. Il en est de même avec les commentaires.
Je crois que des commentaires devraient être ajoutés à chaque fonction, y compris les constructeurs, à chaque propriété de classe, constante statique et à chaque classe. C’est une question de discipline. Lorsque vous autorisez la paresse en autorisant des exceptions aux commentaires lorsque "quelque chose ne nécessite pas de commentaire, car c'est explicite", la paresse est souvent ce que vous obtiendrez.
Quoi que vous pensiez lors de l’implémentation de la fonctionnalité (pertinente pour le travail !), c’est une bonne chose à écrire dans les commentaires. Surtout comment le tout fonctionne, comment une classe est utilisée, quel est le but de cette énumération et ainsi de suite. Le but est très important, car il est difficile d’expliquer par une désignation correcte, à moins que quelqu'un connaisse déjà les conventions à l’avance.
Je comprends que les « InjectorToken » ont tout leur sens pour vous et que vous pourriez le considérer comme "explicites". Franchement c’est un bon nom. Mais quand je vois cette classe je souhaite savoir à quoi sert le jeton, ce qu’il fait, comment je peux l’utiliser et quel est ce truc d’injecteur. Ce serait parfait de voir ça dans les commentaires de sorte que personne n’ait à regarder dans toute l’application, n’est-ce pas ?
XXIV. La documentation▲
Je sais, je sais, je déteste aussi écrire de la documentation. Si vous écrivez tout ce que vous savez dans vos commentaires, alors la documentation peut éventuellement être automatiquement générée par des outils. De plus, la documentation peut vous donner un moyen rapide de rechercher des informations importantes sur le fonctionnement attendu de l’application.
Vous pouvez utiliser Doxygen pour la génération automatique de la documentation.
XXV. Conclusion▲
J’ai préféré un ensemble de principes et non des règles, car j’estime qu’il y a plusieurs façons d’avoir raison. Si vous êtes convaincus que tout devrait être abstrait, essayez. Si vous croyez que les principes SOLID doivent être utilisés dans chaque application ou que si une solution n’est pas réalisée selon un patron de conception bien connu alors elle est immédiatement mauvaise, ça me va.
Choisissez le chemin qui semble correct pour vous et votre équipe, et respectez-le. Et si vous êtes déjà d'humeur expérimentale, essayez certaines des choses mentionnées dans cet article. J'espère que cela améliorera la qualité de votre travail. Merci pour la lecture et pensez à partager si vous avez trouvé l’article intéressant.
XXVI. Note de la rédaction de Developpez.com▲
Ce guide est une traduction de « 23 guidelines for writing readable code ». Nous tenons à remercier Artur Smiarowski qui nous a aimablement autorisé à le traduire et à le publier.