Clean code - noelno/dovelei GitHub Wiki

Nommage des entités

DO

  • donner des noms explicites aux variables, non abrégés et prononçables, reflétant précisément ce qu'il y a dedans. Ne pas hésiter à donner de longs intitulés si nécessaire.
  • réserver les noms de variables courts aux tout petits scopes. Plus la variable est globale, plus elle doit être facile à distinguer.
  • dans les comparaisons ou les indices de tableau, remplacer les valeurs numériques par des constantes ou variables aux noms explicites. Faire un « remplacer tout » devient beaucoup plus facile.
  • dans le cas de classes, les comparaisons peuvent être déléguées à des méthodes au nom explicite, par exemple le prédicat flaggedCells.isFlagged() se contente de retourner le résultat d'une comparaison d'une propriété de l'objet.
  • le nom d'une classe est un nom commun ou un groupe nominal. Il indique ce que l'objet est, pas ce qu'il fait.
  • le nom d'une méthode contient un verbe. Il indique ce que la méthode fait.
  • préfixer les accesseurs, les mutateurs et les prédicats respectivement par get, set et is.
  • si vous utilisez le Factory Pattern, nommez votre méthode statique de factory avec un nom qui explicite le rôle de l'argument passé. (new HondaCar = carFactory::fromBrand( "Honda" ) ) On aurait pu nommer fromBrand() getCarFromBrand() mais on serait tombé dans l'écueil du préfixage inutile, étant donné qu'on attend de toutes les méthodes de factory qu'elles nous retournent une instance de classe vu que c'est son rôle en tant que factory.
  • se restreindre à un mot par concept : pour les méthodes de récupération de données, utiliser le préfixe get OU fetch mais jamais les deux (sauf s'il a été clairement été établi que les deux ont un sens différent).
  • utiliser des termes techniques que tous les développeurs comprennent, par exemple le nom du design pattern utilisé (class TrucmucheFactory), le type d'algorithme de tri mis en place, des termes mathématiques…
    A défaut, utiliser le jargon du cœur de métier ciblé.
  • encapsuler dans une classe les variables qui "vont ensemble" : par exemple $player.name et $player.score sont plus parlants que $name et $score.
  • n'ayez pas peur de renommer une entité mal nommée parce que vous craignez de désorienter les autres développeurs. La plupart du temps les développeurs ne se rappellent pas du nom des entités avant d'avoir remis le nez dans leur code.

DON'T

  • utiliser un mot-clé du langage dans le nom des entités (confusant). Même si votre variable est un tableau, évitez de l'appeler $arrayTrucmuche (notation hongroise), c'est redondant et crée le risque de se retrouver avec des variables doublons (en l'occurence $trucmuche et $arrayTrucmuche).
  • donner aux variables des noms se ressemblant trop, surtout si le nom est long (risque de confondre les deux en lisant vite).
  • les noms où certains caractères peuvent prêter à confusion (0 et O, l et I) et dont le sens ne permet pas de deviner tout de suite le caractère.
  • les noms à l'arrache et/ou qui n'ont pas de sens (par exemple en reprenant le même nom en y accolant un "2" ou en l'orthographiant différemment), parce que le nom que l'on voulait utiliser existait déjà.
  • utiliser des mots "bruits" comme "data", "quantite", "info" : customerInfo ou customerData peuvent être réduits à customer. En se tenant à cette règle on évitera les noms doublons.
  • préfixer les variables relevant de certaines catégories, par exemple les paramètres d'une méthode ("the_" comme "the_name"), pour les variables locales ("a_" comme "a_name") ou pour les champs d'un objet ("_"). Les IDE modernes vous permettent de le savoir sans avoir besoin de préfixe.
  • les noms issus d'une private joke, de références culturelles, d'argot etc…
  • utiliser les mêmes noms de méthode d'une classe à une autre alors que celles-ci n'effectuent en réalité pas le même type de traitement.

Fonctions

DO

  • Faire de très petites fonctions : 3 lignes d'instructions voire moins (les structures conditionnelles comptent pour une ligne).
  • Le niveau d'indentation doit rester de deux maxi.
  • « une fonction ne doit faire qu'une chose » : techniquement une fonction fait toujours plusieurs choses puisqu'il s'agit d'une suite d'instructions. L'astuce est de déterminer si une partie des traitements peut être déléguée a une deuxième fonction donc le nom n'est pas qu'une reformulation de la première.
  • Les structures conditionnelles qui effectuent des traitements différents selon le type de l'objet devraient rester confinées dans les factories.
  • Donner aux fonctions des noms descriptifs, quitte à ce qu'ils soient longs.
  • Garder le même vocabulaire en nommant les entités, pour que l'« histoire » soit plus facile à comprendre
  • Utiliser un nombre réduit de paramètres. A chaque appel de fonction dans le code, on doit deviner ce que la fonction va faire de ces paramètres. Plus il y en a, plus c'est difficile à deviner.
    S'il y en a zéro, aucun travail de réflexion, tout est dans le nom de la fonction.
    S'il y en a un, c'est soit qu'un booléen d'information sur le paramètre est attendu, soit que le paramètre est transformé en autre chose (nouvelle valeur de retour). Plus rarement c'est qu'il s'agit d'un événement.
    S'il y en a deux, c'est déjà plus difficile, sauf s'il s'agit de paramètres qu'on a l'habitude de voir en binôme (par ex. des coordonnées)
    Idéalement embarquez dans un champ de l'objet la valeur que vous vouliez passer en paramètre.
    Si ce n'est pas possible, regroupez en un seul paramètre tous les paramètres si cela a du sens de les regrouper ensemble (toujours l'exemple des coordonnées, qui peuvent être regroupées dans un objet Coord aux propriétés x et y)
  • Une fonction à un paramètre devrait se lire comme une phrase simple verbe-complément : write(name);
  • Une fonction à plusieurs paramètres devrait comporter l'ordre des arguments dans son nom : assertExpectedEqualsActual(expected, actual)
  • Privilégier les exceptions aux codes d'erreur
  • Mettre chaque try catch dans une fonction dédiée (la fonction fait donc plusieurs choses ? Non, elle fait de la gestion d'erreur).
  • Idealement les fonctions ne devraient avoir qu'une sortie. Si vous le pouvez, ne faites qu'un seul return et n'utilisez pas break et continue.
  • Écrivez vos fonctions comme si vous jetiez un brouillon, puis reformulez les progressivement

DON'T

  • mélanger différents niveaux d'abstraction : le code doit se lire comme ceci "Pour faire A, il faut faire B, C et D", puis "Pour faire B, il faut faire E et F" etc...
  • passer un booléen en paramètre : cela implique qu'il y a plus d'un rôle dans la fonction.
  • les effets de bord : la fonction fait quelque chose que son nom n'indique pas, par exemple initialiser une session pour récupérer une valeur. Renommez la fonction pour laisser apparaître cet effet, ou mieux si c'est possible, séparez les traitements.
  • Les fonctions qui renvoient un booléen après avoir effectué un traitement. Une fonction répond à quelque chose (renvoie une info sur l'objet) ou fait quelque chose (change un état de l'objet), jamais les deux, sinon la condition devient difficile à lire.
  • se répéter. Les répétitions sont sources d'erreur, et c'est l'utilité des fonctions d'être répétées.

Commentaires

DON'T : commenter.
Si un commentaire est nécessaire c'est que le développeur a échoué à rendre la portion de code suffisamment claire pour qu'elle s'explique d'elle-même.
Pire encore :

  • les commentaires inutiles : pas clairs, redondants, ajoutés uniquement « parce qu'il fallait bien commenter quelque chose »
  • les commentaires trompeurs, pas actualisés, qui ne reflètent pas exactement ce qu'il y a dans l'entité.
  • l'historique des modifications. les auteurs des différentes modifications : utiliser Github
  • les marqueurs de position ( /* Shortcodes */, /* Configuration globale */…)
  • les commentaires à la parenthèse fermante. Si vous en avez besoin c'est que vos fonctions sont trop longues.
  • les commentaires localisés au mauvais endroit
  • les commentaires avec trop d'informations
  • les commentaires dont on a du mal à comprendre le lien avec le code qui suit
    Tous ces types de commentaires ajoutent du bruit à la page et compliquent la tache au lecteur.

DO
Quelques cas où il est raisonnable de commenter :

  • si la licence d'utilisation oblige à préciser le copyright et/ou le nom de l'auteur
  • décrire le format attendu par la regex (ou mieux, faire une classe utilitaire de regex)
  • la valeur de retour (si son nom n'est pas suffisamment explicite)
  • décrire une intention (pourquoi j'ai besoin que ce bloc fasse ça)
  • avertir des potentielles conséquences de l'utilisation de cette entité
  • TODO - Noter pour mémo ce qui ne va pas dans la fonction et ce qui est prévu pour l'arranger
  • Attirer l'attention sur un élément important qui pourrait passer inaperçu
  • si le programme est ou comporte une API publique (PHPdoc, Javadoc…)

Formatage

  • de 200 à 500 lignes par fichier
  • de 80 à 120 caractères par ligne
  • la métaphore du journal : le nom du module doit être suffisamment parlant pour savoir immédiatement si l'on est au bon endroit, les concepts de haut niveau apparaître en premier et ceux de bas niveau en dernier. La plupart des articles sont tous très petits.
  • les sauts de ligne délimitent le passage d'un concept à un autre
  • les fonctions qui ont un rapport entre elles doivent rester proches dans le fichier
  • les fonctions s'appelant les unes les autres doivent être placées les unes au-dessus des autres (l'appelant au-dessus de l'appelé)
  • déclarer les variables locales au plus près de leur utilisation. Si ce n'est pas possible, utiliser une autre variable locale pour récupérer cette valeur et continuer les traitements
  • déclarer les variables d'instance en début de fichier
  • espacer les opérateurs de comparaison, les + et les - des deux côtés
  • ne pas espacer les arguments de fonction dans la parenthèse
  • aligner les déclarations de variables d'instance est inutile et cache souvent un autre problème : si vous avez besoin d'aligner, c'est que votre classe est trop grosse et que vous devez la fractionner
  • conserver la même convention d'écriture sur tout le fichier

Objets

Les structures de données montrent leur implémentation mais ne font rien de particulier. Les objets cachent leur implémentation de leur donnée mais font plein de traitements dessus.

En procédural (avec des structures de données), on peut facilement rajouter des fonctions sans avoir à modifier les structures de données, contrairement à l'objet où l'ajout d'une fonction abstraite oblige a modifier tous les héritiers et les implémentations.

A l'inverse, en oop on peut facilement ajouter de nouvelles classes sans impacter les fonctions existantes, mais on ne peut pas traiter de nouvelles structures de données en procédural sans avoir à adapter toutes les fonctions existantes.

Les deux sont antinomiques, l'un n'est meilleur que l'autre qu'en fonction du besoin.

  • Ne pas laisser transparaître les détails d'implémentation d'un objet de l'extérieur, utiliser des noms reposant sur l'abstraction de l'objet plutôt qu'un getChamp().
  • Une méthode f d'une instance de classe C ne devrait pouvoir appeler que les méthodes de sa classe ou les méthodes d'un objet de n'importe quelle classe s'il l'a reçue en paramètre, s'il l'a lui-même crée ou s'il le stocke en lui même. A partir du moment où l'objet est sorti de lui-même, il ne doit plus pouvoir appeler ses fonctions. (loi de Demeter). La loi de Demeter ne s'applique qu'aux structures de données

Si vous le pouvez, ne donnez pas de fonction aux structures de données (DTO). Active record est une DTO spéciale

Erreurs

  • Encapsuler le try...catch dans une fonction au nom évocateur et l'appeler dans la partie du code où risque de se déclencher l'erreur
  • Préciser le contexte de l'erreur : quelle opération a échoué, quel type d'erreur, éventuellement tenir un log des erreurs
  • Encapsuler les API est une bonne pratique
  • Retourner un élément vide du type attendu plutôt que null (cf. Special Case Pattern de Fowler)
  • En général une seule classe suffit pour lister les exceptions
  • Ecrire le try...catch avant l'implémentation de la fonction (TDD)

Code tiers

  • Masquer ce code derrière une interface codée par soi-même
  • Coder des "tests d'apprentissage" pour vérifier que le comportement que vous attendez de l'API est bien le sien, et et pour vérifier rapidement que l'API fonctionne toujours comme prévu aux futures udpates
  • Si ce code tiers n'existe pas encore, on peut quand même coder une "interface" en fonction de ce qui est attendu du futur code tiers.

TDD

Les tests permettent de modifier le programme sans craindre de casser quelque chose plus loin sans s'en apercevoir.

  • Les test sont écrits dans l'ordre suivant :
    A. Ecrire un code de test
    B. Ecrire un code de production tout juste suffisant pour passer le test
  • Les tests doivent être aussi lisibles que le reste du programme
  • Les tests ne sont utilisés qu'en développement, pas en production, donc la problématique des performances ne se pose plus
  • Chaque test doit être séparé en trois blocs : Build -> Operate -> Test
  • Utiliser le moins d'assert possible
  • Un test : un concept
  • Le test doit pouvoir s'exécuter rapidement
  • Le test fonctionne indépendamment des autres tests
  • Les tests fonctionnent sur n'importe quel environnement de test
  • Ils retournent obligatoirement true ou false

Classes

  • Déclarations en début en fichier : les constantes statiques publiques en 1er, puis les constantes statiques privées, puis les variables statiques privées
  • Limiter les variables en visibilité "protected" ou "package" aux variables de test, s'il est impossible de faire autrement.
  • Une responsabilité par classe = une seule raison de changer.
    Cette responsabilité peut être décrite en quelques mots (25 max) sans conjonction de coordination, et par un nom concis (sans utiliser les termes "manager", "processor", "super"...)
  • Toutes les variables d'instance devraient être utilisées par toutes les méthodes de la classes, ce qui implique qu'il y ait un nombre réduit de variables d'instance.
    Si certaines variables ne sont utilisées que par quelques méthodes, c'est le signe que vous devriez plutôt fractionner votre classe en plusieurs autres.
  • Si vous savez d'avance que votre classe sera amenée à grossir avec le temps (par exemple une classe SQL qui n'implémente pas encore les requêtes UPDATE parce que pas encore eu besoin) c'est aussi le signe que chaque nouvelle entrée devrait plutôt faire partie d'une sous-classe => hériter plutôt que modifier l'existant.
  • Une classe concrète (opposé d'abstraite) peut dépendre de données externes que l'on ne maîtrise pas toujours, par exemple les taux de change, ce qui rend l'écriture d'un test compliquée puisque la valeur attendue change constamment. Pour limiter cette dépendance, on aura recours à une interface intermédiaire contenant une seule méthode, et qui renverra un résultat précis pendant la phase de test.

Systèmes

Separation of Concerns : un module, des petites classes, un rôle. Relation entre les objets : limiter les dépendances écrites en dur. Préférer l'injection de dépendance et/ou une approche AOP (Aspect-Oriented Programming)