L'une des tâches courantes en développement d'applications consiste à valider l'intégrité des données, notamment en s'assurant que certains champs obligatoires ne sont pas nuls. Dans des scénarios complexes où la liste des champs à vérifier est configurable (par exemple, via une base de données), l'écriture d'un code robuste et maintenable peut s'avérer délicate. Cet article explore comment le type dynamic de C# peut être utilisé pour refactoriser un code de validation initialement répétitif en une solution élégante et entièrement dynamique.
Le Problème Initial : Répétition et Rigidité
Considérons un scénario où nous devons vérifier la non-nullité de champs spécifiques dans plusiuers entités de données. La configuration des champs à valider pour chaque entité est définie dans une liste de modèles. Le code initial est souvent écrit de manière très directe, conduisant à des blocs de code quasi identiques répétés pour chaque entité.
Voici un exemple illustratif de cette approche, où des blocs de vérification sont dupliqués pour chaque propriété d'une entité principale (comme Produits et Ventes dans notre exemple abstrait) :
// Définitions de classes simplifiées pour l'exemple
public class DonneesReference
{
public string ValeurCode { get; set; } // Identifiant unique, ex: "EntiteProduit"
public string NomAffichage { get; set; } // Nom affiché, ex: "Section Produits"
}
public class DetailChampConfiguration
{
public string IdentifiantChamp { get; set; } // Nom de la propriété, ex: "CodeProduit"
public string NomChamp { get; set; } // Nom affiché du champ, ex: "Code Produit"
public bool EstRepertoire { get; set; } // Indique si c'est un groupe de champs
}
public class ConfigurationEntite
{
public DonneesReference ReferenceEntite { get; set; }
public List<DetailChampConfiguration> ChampsAssocies { get; set; }
}
// Entités de données de l'application
public class EntiteProduit { public string CodeProduit { get; set; } public decimal Quantite { get; set; } /* ... */ }
public class EntiteVente { public string IdentifiantClient { get; set; } public decimal MontantTotal { get; set; } /* ... */ }
// Conteneur principal des données
public class DonneesApplication
{
public EntiteProduit Produits { get; set; }
public EntiteVente Ventes { get; set; }
// ... d'autres sections de données ...
}
public class ValidateurAncienStyle
{
public string VerifierIntegriteAncienneMethode(DonneesApplication donnees, IList<ConfigurationEntite> configurations)
{
var champsManquants = new List<string>();
// --- Bloc de vérification pour l'entité 'Produits' ---
if (donnees.Produits != null)
{
var instanceProduit = donnees.Produits;
string nomEntiteProduit = instanceProduit.GetType().Name;
foreach (var config in configurations)
{
if (config.ReferenceEntite.ValeurCode.Replace("_", "") == nomEntiteProduit)
{
foreach (var champDetail in config.ChampsAssocies)
{
var prop = instanceProduit.GetType().GetProperty(champDetail.IdentifiantChamp);
if (prop != null)
{
object valeurChamp = prop.GetValue(instanceProduit);
if (valeurChamp == null)
{
champsManquants.Add($"{config.ReferenceEntite.NomAffichage}.{champDetail.NomChamp}");
}
}
}
}
}
}
// --- Bloc de vérification pour l'entité 'Ventes' ---
if (donnees.Ventes != null)
{
var instanceVente = donnees.Ventes;
string nomEntiteVente = instanceVente.GetType().Name;
foreach (var config in configurations)
{
if (config.ReferenceEntite.ValeurCode.Replace("_", "") == nomEntiteVente)
{
foreach (var champDetail in config.ChampsAssocies)
{
var prop = instanceVente.GetType().GetProperty(champDetail.IdentifiantChamp);
if (prop != null)
{
object valeurChamp = prop.GetValue(instanceVente);
if (valeurChamp == null)
{
champsManquants.Add($"{config.ReferenceEntite.NomAffichage}.{champDetail.NomChamp}");
}
}
}
}
}
}
return string.Join(", ", champsManquants);
}
}
Cette approche, bien que fonctionnlele, souffre de plusieurs défauts majeurs : une forte duplication de code, l'utilisation de noms de propriétés "en dur" (bien que masqués par la configuration, la structure est répétée), et une mauvaise extensibilité. L'ajout d'une nouvelle entité à vérifier nécessiterait l'ajout d'un nouveau bloc de code similaire.
Première Étape de Refactorisation : Introduction du dynamic pour une Réutilisation Partielle
L'introduction du mot-clé dynamic en C# 4.0 offre une opportunité de réduire la duplication. Nous pouvons extraire la logique de vérification des champs d'une entité dans une méthode auxiliaire qui accepte un objet de type dynamic. Cela permet à la méthode d'opérer sur n'importe quel type d'entité sans connaître son type exact à la compilation.
public class ValidateurSemiRefactore
{
private string ExaminerChampsEntite(dynamic entiteDynamique, IList<ConfigurationEntite> configurationsChamps)
{
var champsManquantsDansEntite = new List<string>();
string nomObjetDynamique = entiteDynamique.GetType().Name; // Obtient le nom réel du type à l'exécution
foreach (var config in configurationsChamps)
{
// Fait correspondre la configuration par la valeur du code (ex: "EntiteProduit")
if (config.ReferenceEntite.ValeurCode.Replace("_", "") == nomObjetDynamique)
{
foreach (var champDetail in config.ChampsAssocies)
{
// Utilisation de la réflexion sur le type réel de l'objet dynamique
var propriete = entiteDynamique.GetType().GetProperty(champDetail.IdentifiantChamp);
if (propriete != null)
{
object valeur = propriete.GetValue(entiteDynamique);
if (valeur == null)
{
champsManquantsDansEntite.Add($"{config.ReferenceEntite.NomAffichage}.{champDetail.NomChamp}");
}
}
}
// Une fois la configuration trouvée et traitée pour cette entité, on peut arrêter
return string.Join(", ", champsManquantsDansEntite);
}
}
return string.Empty; // Aucune configuration correspondante ou aucun champ manquant
}
public string VerifierIntegriteSemiRefactore(DonneesApplication donnees, IList<ConfigurationEntite> configurations)
{
var erreursGlobales = new List<string>();
// Appel de la méthode auxiliaire pour chaque entité connue
if (donnees.Produits != null)
{
string resultatProduits = ExaminerChampsEntite(donnees.Produits, configurations);
if (!string.IsNullOrEmpty(resultatProduits))
{
erreursGlobales.Add(resultatProduits);
}
}
if (donnees.Ventes != null)
{
string resultatsVentes = ExaminerChampsEntite(donnees.Ventes, configurations);
if (!string.IsNullOrEmpty(resultatsVentes))
{
erreursGlobales.Add(resultatsVentes);
}
}
return string.Join("; ", erreursGlobales);
}
}
Cette refactorisation réduit significativement le code dupliqué pour la logique de vérification des champs, mais la méthode principale (VerifierIntegriteSemiRefactore) contient toujours des appels "en dur" aux propriétés donnees.Produits et donnees.Ventes. L'extensibilité reste un problème, car l'ajout d'une nouvelle entité nécessiterait toujours une modification de cette méthode.
Refactorisation Complète : Validation Entièrement Dynamique
Pour atteindre une solution entièrement dynamique, nous devons éliminer les références "en dur" aux propriétés de l'objet DonneesApplication. Cela peut être réalisé en parcourant d'abord la liste des ConfigurationEntite, puis en utilisant la réflexion et le type dynamic pour accéder aux propriétés correspondantes de l'objet DonneesApplication.
// Constante pour le champ 'EstRepertoire'
public static class ConstantesValidation
{
public const bool EstNiveauRepertoire = true;
}
public class ValidateurDynamique
{
// Méthode auxiliaire pour vérifier les sous-champs d'un objet dynamique
private string VerifierSousChampsEntite(dynamic sousEntite, IList<DetailChampConfiguration> configsSousChamps)
{
var champsNulsTrouves = new List<string>();
Type typeSousEntite = sousEntite.GetType(); // Obtient le type concret de l'objet dynamic
foreach (var champConfig in configsSousChamps)
{
// Si le champ est un niveau répertoire (sans valeur directe à vérifier), on l'ignore
if (champConfig.EstRepertoire == ConstantesValidation.EstNiveauRepertoire)
{
continue;
}
var propriete = typeSousEntite.GetProperty(champConfig.IdentifiantChamp);
if (propriete != null)
{
object valeur = propriete.GetValue(sousEntite);
if (valeur == null)
{
champsNulsTrouves.Add(champConfig.NomChamp);
}
}
}
return string.Join(", ", champsNulsTrouves);
}
// Méthode principale pour une validation entièrement dynamique
public string ValiderIntegriteDynamiquement(DonneesApplication donneesPrincipales, IList<ConfigurationEntite> configurationsGlobales)
{
var messagesErreur = new List<string>();
Type typeDonneesPrincipales = donneesPrincipales.GetType();
foreach (var configEntite in configurationsGlobales)
{
string identifiantProprietePrincipale = configEntite.ReferenceEntite.ValeurCode; // Ex: "Produits", "Ventes"
// Accède dynamiquement à la propriété correspondante de l'objet principal
// Le résultat est de type dynamic pour permettre l'appel à GetProperty().GetValue() plus tard
dynamic valeurProprietePrincipale = typeDonneesPrincipales.GetProperty(identifiantProprietePrincipale)?.GetValue(donneesPrincipales, null);
// Si la propriété/entité principale est elle-même nulle
if (valeurProprietePrincipale == null)
{
messagesErreur.Add($"L'entité '{configEntite.ReferenceEntite.NomAffichage}' est manquante.");
continue;
}
// Si la propriété/entité n'est pas nulle, vérifie ses sous-champs en utilisant la méthode auxiliaire
string resultatsSousChamps = VerifierSousChampsEntite(valeurProprietePrincipale, configEntite.ChampsAssocies);
if (!string.IsNullOrEmpty(resultatsSousChamps))
{
messagesErreur.Add($"Champs nuls dans '{configEntite.ReferenceEntite.NomAffichage}': {resultatsSousChamps}");
}
}
return string.Join("; ", messagesErreur);
}
}
Cette version finale représente une solution véritablement dynamique. Elle itère à travers les configurations fournies par la base de données, utilise la réflexion pour accéder aux propriétés correspondantes de l'objet principal, puis délègue la vérification des sous-champs à une méthode auxiliaire qui, elle aussi, utilise dynamic et la réflexion. Il n'y a plus de code "en dur" faisant référence à des noms de propriétés spécifiques dans la logique de validation. L'ajout ou la suppression d'entités ou de champs à valider peut être géré entièrement par la configuration, sans modifier le code de validation lui-même.