Double Dispatch C# : Quand l'injection de dépendances a du sens - Expliqué par l'exemple de conception pilotée par le domaine de Derek Comartin
Dans le domaine du langage de programmation C#, la double répartition est souvent mal comprise ou sous-utilisée. Il s'agit d'une technique puissante qui permet un comportement polymorphe entre les deux objets concernés, en particulier lorsqu'il s'agit de gérer le comportement de classes dérivées.
Dans sa vidéo "Double Dispatch in DDD : When Injecting Dependencies Makes Sense", Derek Comartin explique comment le double dispatch s'intègre parfaitement dans la conception pilotée par les domaines (DDD). Nous explorerons ses exemples en profondeur, en montrant comment il peut réduire la charge de maintenance, simplifier le code existant et même imiter des modèles tels que le modèle du visiteur que l'on trouve couramment dans d'autres langages.
Si vous avez déjà eu affaire aux limites de la répartition simple en C#, ou essayé de déterminer un comportement basé à la fois sur l'instance d'un objet et sur une stratégie ou une règle injectée, alors cet article vous aidera à clarifier la façon dont la répartition double C# peut être utilisée efficacement.
Pourquoi l'injection de comportements est judicieuse dans les modèles de domaine
Derek commence par rappeler un dogme courant dans le DDD, à savoir que votre modèle de domaine ne doit avoir aucune dépendance. Mais ce n'est pas toujours pratique ou utile. Lors de la modélisation du comportement, il peut être nécessaire d'injecter de la logique telle que des règles, des politiques ou des stratégies. C'est là que la double répartition est utile : vous pouvez passer un concept de domaine, comme une politique, dans l'objet de domaine, et laisser l'évaluation basée sur la méthode être gérée en externe, tout en conservant le contrôle final de l'objet de domaine.
Ce modèle est logique si l'on considère ce que l'on couple : le comportement du domaine, et non l'infrastructure.
Le mauvais exemple : Logique codée en dur dans le domaine
Pour illustrer le problème, Derek commence par une classe d'expédition qui contient une logique codée en dur pour déterminer les retards :
public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}
public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}
Le code existant est simple mais peu flexible. Elle s'appuie sur des décisions prises au moment de la compilation et associe étroitement le comportement à la classe. Pour modifier la règle, il faudrait changer le modèle du domaine, et les tests sont plus difficiles car ils dépendent du temps.
Le Refactor : Utilisation des politiques et de la double répartition
Derek présente l'interface IDeliveryTimingPolicy :
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}
Il crée ensuite deux implémentations :
-
Politique de délai de livraison standard
- BufferedDeliveryTimingPolicy (Politique de délai de livraison)
Ces classes prennent en compte l'envoi et renvoient un booléen en fonction de leur règle. Voici un extrait de code de BufferedDeliveryTimingPolicy :
public bool IsLate(Shipment shipment)
{
return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}
public bool IsLate(Shipment shipment)
{
return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}
Dans la classe Shipment, nous utilisons maintenant double dispatch :
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}
Il s'agit d'un double envoi classique : le premier envoi consiste à appeler IsLate() sur l'envoi et à transmettre la politique ; le deuxième envoi consiste à appeler IsLate() sur la police avec l'envoi comme paramètre. Deux objets sont impliqués, chacun prenant part à la détermination du comportement - ce qu'un simple dispatch ne peut pas faire.
Avantages en matière de test et de flexibilité
Cette approche permet d'obtenir un code hautement testable et déterministe. Derek montre des exemples utilisant des politiques standard et tamponnées, où les données de test sont contrôlées et où le type d'exécution de chaque objet détermine le comportement.
var policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute buffer
var policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute buffer
Ceci démontre comment le comportement de l'exécution peut être modifié sans changer le modèle du domaine. Vous obtenez un comportement polymorphe basé sur la nature dynamique de la politique, tout en maintenant l'intégrité du domaine.
Double Dispatch vs Visitor Pattern en C
L'exemple de Derek ressemble au modèle du visiteur, qui consiste à définir un ensemble d'opérations à effectuer sur des objets sans en modifier la structure. Typiquement, dans le modèle du visiteur, vous verriez quelque chose comme :
public abstract class Shape
{
public abstract void Accept(IVisitor visitor);
}
public class Circle : Shape
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public abstract class Shape
{
public abstract void Accept(IVisitor visitor);
}
public class Circle : Shape
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Ici, les méthodes Accept() et Visit() sont coordonnées entre deux types - l'objet et le visiteur - tout comme Derek utilise Shipment et IDeliveryTimingPolicy. Ce modèle aide à mettre en œuvre un comportement de répartition multiple, même dans un langage de répartition unique comme C#.
Composition de règles avec des collections de politiques
Dans un autre exemple, Derek montre comment plusieurs règles peuvent être évaluées via une collection. Il introduit des règles telles que :
-
HasValidDestinationRule
-
Règle AllPackagesPacked
- Règle des produits non déjà expédiés
Chacun met en œuvre IShipmentReadinessRule avec :
public bool IsSatisfiedBy(Shipment shipment)
public bool IsSatisfiedBy(Shipment shipment)
Ensuite, la classe Expédition les évalue de la manière suivante :
public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
return rules.All(rule => rule.IsSatisfiedBy(this));
}
public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
return rules.All(rule => rule.IsSatisfiedBy(this));
}
Ce modèle void accept vous permet d'évaluer plusieurs règles de domaine de manière dynamique. Si vous stockez des règles dans la configuration (en particulier dans une application multi-tenant), vous pouvez créer une nouvelle liste au moment de l'exécution et la transmettre à l'envoi.
Règles dynamiques dans les applications multi-locataires
Derek souligne que dans les applications multi-locataires, l'ensemble des règles peut changer pour chaque client. Vous pourriez récupérer une liste de politiques à partir du stockage et les injecter dynamiquement :
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
Cela montre comment la répartition dynamique et la prise de décision au moment de l'exécution peuvent être intégrées à votre application. Le comportement souhaité est obtenu en modifiant la configuration, et non le modèle.
Éclaircir les idées fausses : Les dépendances ne sont pas toutes mauvaises
Vers la fin, Derek s'attaque à l'idée fausse selon laquelle il est mauvais d'injecter quoi que ce soit dans un domaine. Il insiste sur le fait que ce qui compte, c'est ce que vous injectez. Injecter le comportement d'un domaine, comme des spécifications ou des politiques, n'est pas la même chose qu'injecter de l'infrastructure.
Le modèle de domaine reste le point d'entrée. Il détient la décision mais peut la déléguer à d'autres objets, tant qu'ils se trouvent dans le même contexte de domaine.
Enveloppe : Pourquoi le Double Dispatch C# est puissant dans le DDD
Derek termine en nous invitant à faire preuve d'esprit critique : n'ayez pas peur de void visit, public virtual void accept ou d'autres modèles similaires lorsqu'ils apportent de la clarté et de la maintenabilité. Lorsque vous injectez de la logique d'entreprise de manière contrôlée, vous gagnez en flexibilité et en précision.
Ainsi, que vous travailliez sur une nouvelle classe ou que vous remaniiez une base de code existante, le double dispatch C# vous offre un moyen propre de séparer les préoccupations tout en maintenant l'accent sur le domaine.
Si vous utilisez des politiques, des spécifications ou même si vous construisez un modèle de visiteur sans vous en rendre compte, vous êtes déjà sur le point de mettre en œuvre la double répartition. La compréhension de ces outils vous permet de mieux contrôler le comportement du code au moment de l'exécution, ce qui améliore la testabilité et l'adaptabilité.
Conclusion
En résumé : le double dispatch en C# peut être une solution élégante et pratique pour injecter la logique du domaine, préserver l'encapsulation et prendre en charge un comportement flexible. Utilisé avec des modèles tels que visitor, multiple dispatch et dynamic keyword use (carefully), il permet d'écrire des modèles de domaine expressifs et robustes.
Ainsi, la prochaine fois que vous appellerez shipment.IsLate(policy)-sachez que vous tirez parti d'un motif fondamental qui rapproche C# d'une conception véritablement polymorphe.
Exemple de conseil : si vous créez une classe PurchaseOrder et que vous souhaitez déterminer si un élément peut être ajouté en fonction d'une politique, essayez de transmettre la politique au PurchaseOrder et laissez la politique visiter la commande. C'est la double répartition en action.
Regardez la vidéo complète vidéo sur son YouTube Channel et obtenez plus d'informations sur le sujet.
