Ce smell code est couramment rencontré. Son traitement permet de simplifier drastiquement le code d'une application en capturant une grande part de sa complexité.
Introduction
En programmation, les types primitifs correspondent aux types de base utilisés dans un langage. En C# les types primitifs sont les suivants :
bool, byte, sbyte, char, decimal, double, float, int, uint, long, ulong, object, short, ushort, string
Beaucoup de développeurs que j'ai eu l'occasion de croiser dans mes missions, du plus junior au plus expérimenté, sont réticents à utiliser de petits objets : montant, interval, date non horodatée, coordonnées (X, Y), pourcentage...
Le retours sont du genre "mais, tu ré-inventes la roue !" ou encore "on voit bien qu'il s'agit d'un montant puisque la variable est nommée mntBidule !" ou encore "mais c'est une perte de temps, c'est juste une chaîne de caractères"...
Comme nous allons le voir il est vraiment dommage de se passer de ces petits objets. En effet, ce sont eux qui capturent une grande partie de la complexité d'une application.
Symptômes
- Les méthodes prennent en paramètre essentiellement des types primitifs
- Les champs (trop nombreux) d'une classe ne sont composés essentiellement que de types primitifs
- Les concepts métiers (montant, date, numéro de téléphone) sont représentés par des types primitifs
En bref, le code est truffé de types primitifs.
Conséquences
- Violation du principe Open / Closed de SOLID
- La logique métier associée aux valeurs représentées par les types primitifs n'est pas encapsulée
- Aucune contrainte forte ne permet d'assurer la validité de la variable à tout instant
Traitement
Dans son livre Refactoring, Martin Fowler propose les refactorings suivant :
- Replace Primitive with Object
- Replace Type Code with Class
- Replace Type Code with Subclasses
- Replace Type Code with State / Strategy
- Extract Class
- Introduce Parameter Object
- Replace Array with Object
Ces refactorings feront chacun l'objet d'un article. En attendant, vous pouvez retrouver l'ensemble des définitions de ceux-ci sur le site de Martin Fowler.
Bénéfices
- Complexité capturée au sein de petits objets testables facilement de façon unitaire
- Suppression de duplication de code
- Simplification du code / code plus lisible
- Code fortement typé rendant explicite les notions métiers
Exemple
Dans une application qui utilise des pourcentages, combien de fois allez vous retrouver ce genre de code (notamment ligne 3) :
double mntAvantDiscount = CalcMnt(); double pctRemise = GetPctRemise(); double mntApresDiscount = mntAvantDiscount * (1 - pctRemise / 100);
On voit que l'on ne traite qu'avec des doubles. Le calcul du pourcentage, qui relève du domaine des mathématiques, est répété encore et encore à travers le code. Ci-dessous un exemple de code où les doubles ont été remplacé par des objets valeur.
Montant montantBrut = CalculerMontant(); Pourcentage remise = ObtenirRemise(); Montant montantRemise = montantBrut.DiminuerDe(remise);
public class Pourcentage { public static Pourcentage DepuisCentiéme(float valeur) { return new Pourcentage(valeur); } public static Pourcentage DepuisValeurNumérique(float valeur) { return new Pourcentage(float * 100); } private readonly float _valeur; private Pourcentage(float valeur) { _valeur = valeur; } public float Diminuer(float valeur) { return valeur * (1 - ObtenirDecimal()); } public float ObtenirDecimal() { return _valeur / 100; } // ... } public class Montant { // ... private readonly float _montant; public Montant(float montant) { _montant = montant; } public Montant DiminuerDe(Pourcentage pourcentage) { float valeurMontantDiminué = pourcentage.Diminuer(_montant); return new Montant(valeurMontantDiminué); } }
On voit apparaître des interactions entre les différentes types. Ici on voit qu'un montant peut-être diminué d'un pourcentage et que le résultat de cette opération est un montant.
Je vous laisse imaginer maintenant toutes les interactions entre une multitude de différents types. Prenons le domaine de la CAO par exemple : points, coordonnées X, coordonnées Y... Et oui, il ne doit pas être possible de soustraire une coordonnée X à une coordonnée Y, mais la soustraction d'une coordonnée X à une autre retourne une distance composée elle même d'une valeur et d'une unité qui ne pourra elle même pas être additionnable avec une distance d'unité différente qui....
Reprenons notre exemple de pourcentages. Il s'agit d'un cas simple, celui du domaine des mathématiques connus par tous les développeurs. Mais dans le cas d'un domaine métier spécifique maîtrisé par peu de gens, toute information est bonne à prendre !
Passons maintenant à l'échelle de toute une application (ou tout du moins d'un bounded context). On comprend bien que beaucoup de la complexité diffuse va être capturée et il va être facile de couvrir de test ces objets à 100%. Le principe DRY est appliqué et on limite drastiquement l'apparition de bugs.
Certains diront (à tort ou raison, cela peut dépendre du domaine) : "Quoi ? Un type float pour exprimer un montant ou un pourcentage !!!" Qu'à cela ne tienne : la modification du code de l'application / bounded context restera ciblée sur les classes Montant et Pourcentage. Les impacts sont alors détectable facilement à la compilation.
Vous avez un doute ? Appliquer cette méthode selon le concept Shuhari pendant quelques itérations et voyez le résultat 😉
Pour aller plus loin...
Je vous invite à regarder cette excellente présentation de @Cyrille Martraire concernant les monoïdes. "Les monokoi ?" Si si, je vous assure il y a un rapport avec cet article. Bonne séance...