Refactoring Conditional if in C# : Éviter l'encombrement conditionnel avec Derek Comartin
En C#, les instructions conditionnelles telles que l'instruction if, l'instruction if else et l'instruction switch sont des outils essentiels. Mais que se passe-t-il lorsque ces constructions sont utilisées de manière excessive, en particulier lorsqu'elles sont liées à des enums ? Derek Comartin, dans sa vidéo "Enums Aren't Evil. Conditionals Everywhere Are", nous présente un remaniement détaillé qui remplace la logique conditionnelle généralisée par des modèles plus propres et plus faciles à maintenir.
Dans cet article, nous allons suivre le raisonnement de Derek étape par étape, en utilisant ses horodatages comme points d'ancrage. Nous examinerons également comment ses idées s'appliquent aux modèles conditionnels courants en C#, tels que l'opérateur ternaire, l'instruction else et les structures switch-case, en mettant en évidence les problèmes qu'ils peuvent poser dans les bases de code volumineuses et la manière dont vous pouvez les remanier pour améliorer la conception.
L'explosion du conditionnel : Le vrai problème
Derek commence par présenter une instruction if qui vérifie le type de produit :
if (productType == ProductType.Template || productType == ProductType.Ebook)
if (productType == ProductType.Template || productType == ProductType.Ebook)
À première vue, le conditionnel if ci-dessus semble simple. Mais Derek prévient que ce type d'instruction évalue une condition donnée et exécute ensuite un bloc de code uniquement si la condition est vraie, ce qui devient problématique lorsque cette logique est répétée partout.
Il se peut que vous ayez à nouveau ce bloc dans une autre méthode ou un autre cours :
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
Derek explique que ce modèle se répand rapidement dans un grand système. La même logique if else apparaît dans plusieurs services, ce qui entraîne des incohérences et des bogues lors de l'ajout d'une nouvelle valeur d'énumération. Par exemple, que se passe-t-il lorsque vous introduisez un nouveau type de produit comme Video ? Vous devrez vous rappeler de mettre à jour chaque bloc où cette expression conditionnelle existe.
La répétition amplifie la complexité
Dans l'exemple suivant, Derek se penche sur les conditionnelles imbriquées. Dans une méthode, une instruction if else vérifie le même enum, et le résultat est transmis à une autre méthode, qui contient également une vérification similaire.
L'instruction vérifie la présence de Template ou Ebook, et renvoie quelque chose - sinon, elle renvoie null. Derek note que cette redondance ne fait pas qu'allonger le code, elle introduit des risques de maintenance. La même logique est copiée dans plusieurs fichiers, ce qui entraîne un chaos dans le flux de contrôle.
Si votre système vous oblige à ajouter un cas par défaut chaque fois que vous touchez à une énumération, vous savez que quelque chose ne va pas.
Changer la façon dont nous pensons aux conditionnels
Au lieu de vérifier constamment les types avec if else, Derek propose de poser une meilleure question :
Le produit présente-t-il des caractéristiques téléchargeables ?
Il s'agit d'une meilleure expression de l'intention. Elle rend votre code plus lisible et réduit la dépendance à l'égard de l'enum. Plutôt que d'écrire une instruction if avec deux conditions comme :
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
Vous pouvez simplement encapsuler cette logique dans un modèle et écrire :
if (product.HasDownloadableResource())
if (product.HasDownloadableResource())
Cela ne se vérifie que lorsqu'une ressource téléchargeable est présente, ce qui réduit la nécessité d'utiliser des expressions conditionnelles complexes.
De la déclaration If au comportement encapsulé
Pour résoudre le problème principal, Derek introduit un type de ressource téléchargeable (DownloadableResource). Ce type de document comprend une URL de téléchargement et un nom de fichier par défaut. Elle devient une partie de premier ordre de votre domaine, plutôt que de s'appuyer sur des instructions if pour la dériver.
Maintenant, au lieu de répéter ceci :
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
Vous écrivez ceci :
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
Cela simplifie considérablement la logique et élimine le besoin de branches de l'instruction else ou même d'une instruction switch.
Le temps de fonctionnement plutôt que le temps de compilation : un changement stratégique
Derek va plus loin en expliquant un choix de conception important : déplacer la logique de la compilation à l'exécution. Cela signifie qu'il faut interroger le système au moment de l'exécution pour savoir si une DownloadableResource existe pour un produit. Si c'est le cas, agissez en conséquence. Si ce n'est pas le cas, passez votre chemin.
Il note que cette démarche transforme la logique statique "if else" en requêtes d'exécution. Elle peut ajouter un appel à la base de données, mais elle réduit la logique imbriquée if else et centralise le comportement. Cela permet d'améliorer la maintenabilité à grande échelle.
Utilisation de l'héritage pour les produits téléchargeables
Derek explore également la voie de l'héritage. Vous pouvez créer une classe de base abstraite Product, puis définir des types dérivés tels que Ebook, Template ou OfflineCourse.
Chacune d'entre elles remplace des méthodes telles que :
public virtual string GetDownloadUrl() { ... }
public virtual string GetDownloadUrl() { ... }
Cette approche permet à chaque produit de gérer sa propre logique. Bien que cette méthode permette d'éviter une instruction switch ou plusieurs instructions conditionnelles, Derek souligne qu'il est toujours possible d'écrire des expressions conditionnelles en interne si l'on n'y prend pas garde.
Meilleure encapsulation sans héritage
Si l'héritage vous semble trop lourd, Derek suggère d'utiliser des types explicites comme DownloadableProduct, qui contient ses propres propriétés et méthodes, sans être lié à une hiérarchie.
Dans votre programme, cela pourrait ressembler à ce qui suit :
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
Il n'est pas nécessaire d'utiliser une instruction if else ou switch pour déterminer le comportement : chaque objet sait ce qu'il doit faire.
Réparation légère : Méthodes d'extension sur les Enums
Si vous n'êtes pas prêt à abandonner les enums, Derek propose une solution légère : créer une méthode d'extension :
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
Maintenant, au lieu d'écrire :
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
Vous pouvez la simplifier comme suit :
if (product.Type.IsDownloadable())
if (product.Type.IsDownloadable())
La traduction doit rester professionnelle et préserver l'exactitude technique tout en expliquant les caractéristiques et les avantages de ces outils de développement, ce qui permet de centraliser votre logique et d'éviter de répéter sans cesse les parenthèses et le bloc de code.
Éviter la surutilisation des opérateurs ternaires et des commutateurs
Derek met également en garde contre l'utilisation excessive d'abréviations telles que l'opérateur ternaire :
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
Bien qu'il s'agisse d'une syntaxe valide, elle peut être sujette à des erreurs et difficile à lire lorsque la logique devient complexe. La traduction doit rester professionnelle et préserver l'exactitude technique tout en expliquant les caractéristiques et les avantages de ces outils de développement.
De même, les commutateurs avec une instruction break et un cas par défaut tombent également dans ce piège. Il est préférable de demander aux objets de se comporter plutôt que d'utiliser une logique de cas de commutation.
Conclusion : Un contrôle plus intelligent avec moins d'encombrement conditionnel
En conclusion, la vidéo de Derek n'est pas une attaque contre les enums, mais une critique de la façon dont nous utilisons les structures conditionnelles if autour d'eux. En généralisant les instructions if else et switch dans votre base de code, vous rendez le système plus difficile à tester, à maintenir et à faire évoluer.
Que vous optiez pour l'encapsulation, la recherche d'exécution, l'héritage ou de simples méthodes d'extension, l'objectif reste le même : réduire les conditionnelles et déplacer la logique là où elle doit être.
N'oubliez pas :
-
Les conditionnels ne sont pas mauvais.
-
L'encombrement conditionnel est.
-
Un code propre ne repose pas sur de multiples instructions if else disséminées dans les classes.
- Évaluez votre contexte et remaniez en conséquence.
Comme le dit Derek, "tout dépend du contexte" Mais une chose est sûre : un produit n'est pas toujours un simple produit - parfois, c'est un signal pour repenser votre conception.
