Ecrire un code de qualité : La programmation par intention

La qualité dans le développement logiciel est un sujet vaste. On parle de « Clean Code » et de « Software Craftsmanship ». Alors, qu’entendons-nous par développement de qualité ?


Une définition suffisamment large qui peut en englober tous ces aspects est selon la suivante : une application est de qualité quand le coût d’une nouvelle fonctionnalité reste stable dans le temps.

La courbe bleue représente une évolution maîtrisée

Dans ce billet, intéressons-nous à une composante de la qualité logicielle : comment exprimer notre intention à travers notre code.

Il s’agit d’écrire du code qui exprime ce que le code fait mais également pourquoi il le fait. Le code se doit d’exprimer des éléments de contexte.

Nous passons 10 fois plus de temps à lire du code qu’à en écrire ¹

Il faut penser long terme. Le code écrit sera utilisé probablement pendant plusieurs années, l’objectif étant que même dans les pires conditions, le code résiste aux bugs.

Il est illusoire de penser qu’une documentation annexe, une spécification, une description de User Story ou des diagrammes d’architecture vont nous fournir les informations nécessaires pour comprendre le système et le modifier sans risque. Ces éléments ne sont tout simplement pas mis à jour en même temps que le code car cela est long et trop coûteux. Seul le code détient la vérité sur le fonctionnement du système. Autant qu’il soit le plus lisible et compréhensible que possible.

L’objectif n’est pas d’écrire du code de qualité parce qu’il sera beau, mais bien pour diminuer le coût de la maintenance. Il s’agit d’investir du temps à court terme pour en gagner à moyen et long terme : économiser du temps et de l’argent.

Exprimer son intention dans son code

Il s’agit pour le développeur de :

  • rendre son code plus expressif
  • ne pas obliger le lecteur à un effort supplémentaire
  • limiter les ambiguïtés et rendre évident un comportement

Voici une synthèse d’un certain nombre de bonnes pratiques Clean Code qui, cumulées favorisent l’expression de l’intention. Les exemples de cet articles sont en C#.

Classes d’action et méthodes : exprimer précisément ce que cela fait

Exemple 1

UpdateInDatabase()

En lieu et place de

ApplyModification()

Exemple 2

class UserCreator { }
class UserUpdator { }

En lieu et place de

class UserService { }
class UserManager { }

Classes Dto/Poco et variable : exprimer précisément ce que cela contient et non pas le contexte d’utilisation

Exemple 1

class IncomeDto { }
class UserDto
{
   IncomeDto Income { get; set; }
}

En lieu et place de

class UserMoney { }

Exemple 2

bool IsEligible { get; set; }

En lieu et place de

bool IsOk { get; set; }

Exemple 3

bool ValidAccessToken { get; set; }

En lieu et place de

bool KeyInformationValidForAccount { get; set; }

Domain Driven Design & ubiquitous language : exprimer le business

Si les Business Analysts / Product Owners / User Stories nous parlent de Cut Off pour gérer des grilles de score, le mot-clé doit se retrouver dans le code (et pas ScoreThreshold) : ne jamais hésiter à solliciter les métiers pour le nommage !

Eviter les acronymes

string MoveLeftToRight(string content)

En lieu et place de

string MoveLTR(string content)

Abuser des énumérés

Les énumérés sont plus expressifs que des booléens ou des codes numériques :

List<OrderFilter> orderFilters = new List<OrderFilter>
{
    OrderFilter.InProgressOrder,
    OrderFilter.AlreadyPaidOrder
};
List<Order> orders = orderRepository.GetOrders(orderFilters);

En lieu et place de

List<Order> orders = orderRepository.GetOrders(true, true);

Les tests doivent nous documenter

ShouldThrowErrorWhenScoreIsKoAndRejectionRulesNotPassed()
ShouldReturnSuccessWhenScoreIsOkAndRejectionRulesPassed()

En lieu et place de

TestScoreAndRules()

Constantes

const string redColor = “#ff0000”;
config.TextColor = redColor;

En lieu et place de

config.TextColor = “#ff0000”;

Gestion des erreurs

Préférer des exceptions métier plutôt que des codes de retour

try
{
    var myGreatServiceResult = await    _greatClient.GetGreatServiceResult(greatServiceData).ConfigureAwait(false);
}
catch (Exception exception)
{
    […]
    throw new StepTechnicalErrorException(“GreatService HTTP call Error”, exception);
}

En lieu et place de

try
{
    var myGreatServiceResult = await _greatClient.GetGreatServiceResult(greatServiceData).ConfigureAwait(false);
}
catch (Exception exception)
{
    […]
    return -1;
}

Découpage et responsabilité unique

Longueur de classe < 300 lignes

Longueur de méthode < 30 lignes (soit une page de code sur un écran standard)

Si ces 2 règles ne sont pas respectées, il y a de fortes chance qu’une classe ou une méthode gère trop de responsabilités, donc un bon candidat aux bugs (tests compliqués dû à la complexité cyclomatique², ajout de nouvelles fonctionnalités plus difficile, lisibilité plus faible).

Exemple vécu

Prenons un exemple réel : un microservice est chargé d’ordonnancer des appels à d’autres microservices avec des règles métiers (moteur de règles). Chaque appel à un microservice est encapsulé dans une classe « *Step ».

Après un refactoring, l’ordonnancement des steps d’un process a été modifié par erreur. En effet certaines steps étaient couplées.

Voici ce à quoi ressemble la portion de code concernée (volontairement simplifiée et renommée) :

Le refactoring partait d’une bonne intention mais le code n’exprimait pas suffisamment son intention ! Or certains processes font des appels externes, coûtant parfois plusieurs euros par appel.

L’anomalie de production est ici clairement dû à un manque de tests unitaires et mais aussi un manque d’intention dans le code.

Aujourd’hui, ce commentaire a été ajouté :

// WARNING: THIS STEPS COMBINES 2 STEPS TOGETHER BECAUSE OF BUSINESS REQUIREMENTS, DO NOT REFACTOR THIS.

Quels sont les problèmes et que pouvons-nous améliorer pour donner l’intention ?

Problèmes :

  • On n’est pas toujours pas SRP (Single Responsability Principle) sur cette classe
  • Les commentaires vieillissent mal
  • Le commentaire n’empêche pas le refactoring raté

Solution :

  • Découper en 2 steps pour devenir SRP, favoriser la lisibilité. La responsabilité devient plus limitée et les classes plus réduites.
  • Ajouter un typage de process « business » (par exemple un enum avec Category.NoMoneyCost et Category.WithMoneyCost
  • Un algorithme se charge ensuite de sélectionner et/ou filtrer les process en fonction de l’enum
  • On y ajoute des tests unitaires pour tous les cas

Exemple concret

Voici un exemple de code sur lequel j’ai eu à me pencher par le passé (légèrement tronqué et simplifié pour les besoins de cet exemple mais l’essentiel est présent).

Pouvez-vous en déduire le contexte et l’objectif de ce programme ?

Difficile n’est-ce pas ?

Au-delà de la qualité intrinsèque du code, l’auteur n’a semble-t-il pas trouvé nécessaire d’exprimer son intention sur ce programme.

J’ai remanié le code iso-fonctionnalité en essayant d’exprimer un maximum d’intention : pouvez-vous désormais en déduire le contexte business ?

En appliquant les règles de base énoncées ci-dessus, on parvient à un code plus clair, lisible, maintenable et évolutif. On en déduit très simplement les règles et le contexte business.

A partir d’un fichier résultant d’une extraction de données brutes, il faut :

  • Concernant les prospects européens : leur envoyer un email s’ils n’ont pas déjà été contactés
  • Concernant les clients inactifs : leur envoyer une alerte dans le CRM si le fournisseur est MNV, si le client n’a pas commandé depuis 2 mois et que son total de commande est supérieur à 10 000€

Conclusion

Le processus d’écriture de code ressemble en certains aspect à l’écriture d’un livre. Le premier jet est souvent mal construit et un peu brouillon. Ensuite nous le retravaillons, restructurons les phrases, adaptons la syntaxe, réorganisons le fil de l’histoire jusqu’à obtenir à un résultat le plus lisible et compréhensible possible.

Nos programmes sont certes destinés à être exécutés et utilisés en production mais aussi à être lus et modifiés par d’autres développeurs en restant robustes et fiables.

“Il est nécessaire que le code résiste à l’épreuve du temps”

(1) “Indeed, the ratio of time spent reading versus writing is well over 10 to 1. — Robert C. Martin

(2) Complexité cyclomatique: Cette mesure reflète le nombre de décisions d’un algorithme en comptabilisant le nombre de « chemins » linéairement indépendants au travers d’un programme