Earn More by Sharing What You Love
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!

Derek Comartin
8m 44s
Im Bereich der Programmiersprache C# wird Double Dispatch oft missverstanden oder nicht ausreichend genutzt. Es handelt sich um eine leistungsstarke Technik, die polymorphes Verhalten zwischen zwei beteiligten Objekten ermöglicht, insbesondere bei der Handhabung von Verhalten über abgeleitete Klassen hinweg.
In seinem Video "Double Dispatch in DDD: When Injecting Dependencies Makes Sense" erklärt Derek Comartin, wie Double Dispatch perfekt in Domain-Driven Design (DDD) passt. Wir werden seine Beispiele eingehend untersuchen und zeigen, wie sie den Wartungsaufwand verringern, bestehenden Code vereinfachen und sogar Muster wie das in anderen Sprachen übliche Besuchermuster nachahmen können.
Wenn Sie jemals mit den Einschränkungen von Single Dispatch in C# zu tun hatten oder versucht haben, das Verhalten sowohl auf der Grundlage der Instanz eines Objekts als auch einer injizierten Strategie oder Regel zu bestimmen, dann wird dieser Artikel dazu beitragen, zu klären, wie Double Dispatch C# effektiv genutzt werden kann.
Derek beginnt mit dem Hinweis auf ein allgemeines Dogma in DDD - dass Ihr Domänenmodell keine Abhängigkeiten haben sollte. Aber das ist nicht immer praktisch oder nützlich. Bei der Modellierung von Verhalten müssen Sie möglicherweise Logik wie Regeln, Richtlinien oder Strategien einfügen. Hier hilft Double Dispatch: Sie können ein Domänenkonzept, z. B. eine Richtlinie, an das Domänenobjekt übergeben und die methodenbasierte Auswertung extern durchführen lassen, wobei das Domänenobjekt weiterhin die endgültige Kontrolle hat.
Dieses Muster macht Sinn, wenn man bedenkt, woran man koppelt: an das Verhalten der Domäne, nicht an die Infrastruktur.
Um das Problem zu veranschaulichen, beginnt Derek mit einer Shipment-Klasse, die eine fest kodierte Logik zur Bestimmung der Verspätung enthält:
public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}Public Function IsLate(expectedDelivery As DateTime) As Boolean
Return _systemClock.Now() > expectedDelivery
End FunctionDer vorhandene Code ist einfach, aber unflexibel. Sie stützt sich auf Entscheidungen zur Kompilierzeit und koppelt das Verhalten eng an die Klasse. Eine Änderung der Regel würde eine Änderung des Domänenmodells erfordern, und das Testen ist schwieriger, da es zeitabhängig ist.
Derek stellt die Schnittstelle IDeliveryTimingPolicy vor:
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}Public Interface IDeliveryTimingPolicy
Function IsLate(shipment As Shipment) As Boolean
End InterfaceAnschließend erstellt er zwei Implementierungen:
Standardlieferzeitplan
BufferedDeliveryTimingPolicy
Diese Klassen nehmen die Sendung auf und geben einen booleschen Wert zurück, der auf ihrer Regel basiert. Der folgende Codeschnipsel stammt aus der BufferedDeliveryTimingPolicy:
public bool IsLate(Shipment shipment)
{
return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}Public Function IsLate(shipment As Shipment) As Boolean
Return _clock.Now() > shipment.DeliveryDate.AddMinutes(30)
End FunctionIn der Klasse "Shipment" verwenden wir nun double dispatch:
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}Public Function IsLate(ByVal policy As IDeliveryTimingPolicy) As Boolean
Return policy.IsLate(Me)
End FunctionDies ist ein klassischer doppelter Versand: Der erste Versand ist der Aufruf von IsLate() für die Sendung und die Übergabe der Richtlinie; der zweite Vorgang ist der Aufruf von IsLate() für die Richtlinie mit der Sendung als Parameter. Es handelt sich um zwei Objekte, von denen jedes an der Bestimmung des Verhaltens beteiligt ist - etwas, das ein einzelnes Dispatch nicht leisten kann.
Dieser Ansatz führt zu einem hochgradig testbaren und deterministischen Code. Derek zeigt Beispiele mit Standard- und gepufferten Richtlinien, bei denen die Testdaten kontrolliert werden und der Laufzeittyp jedes Objekts das Verhalten bestimmt.
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 bufferDim policy = New BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30))
Dim shipment = New Shipment With {.DeliveryDate = DateTime.UtcNow.AddMinutes(-15)}
Assert.False(shipment.IsLate(policy)) ' Because of 30-minute bufferDies zeigt, wie das Laufzeitverhalten geändert werden kann, ohne das Domänenmodell zu verändern. Sie erhalten ein polymorphes Verhalten, das auf der dynamischen Natur der Richtlinie basiert, während die Integrität der Domäne erhalten bleibt.
Dereks Beispiel ähnelt dem Besucher-Muster, bei dem Sie eine Reihe von Operationen für Objekte definieren, ohne deren Struktur zu ändern. Im Besuchermuster sieht man typischerweise etwas wie:
public abstract class Shape
{
public abstract void Accept(IVisitor visitor);
}
public class Circle : Shape
{
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}Public MustInherit Class Shape
Public MustOverride Sub Accept(visitor As IVisitor)
End Class
Public Class Circle
Inherits Shape
Public Overrides Sub Accept(visitor As IVisitor)
visitor.Visit(Me)
End Sub
End ClassHier werden die Accept()- und Visit()-Methoden auf zwei Typen - das Objekt und den Besucher - abgestimmt, genau wie bei Dereks Verwendung von Shipment und IDeliveryTimingPolicy. Dieses Muster hilft bei der Implementierung von Multiple-Dispatch-Verhalten auch in einer Single-Dispatch-Sprache wie C#.
In einem weiteren folgenden Beispiel zeigt Derek, wie mehrere Regeln über eine Sammlung ausgewertet werden können. Er führt Regeln ein wie z. B.:
T>* HasValidDestinationRule
AlleVerpakungenVollständigRegel
Regel "Nicht bereits versendet"
Jeder implementiert IShipmentReadinessRule mit:
public bool IsSatisfiedBy(Shipment shipment)Public Function IsSatisfiedBy(shipment As Shipment) As BooleanAnschließend werden sie von der Klasse Shipment wie folgt ausgewertet:
public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
return rules.All(rule => rule.IsSatisfiedBy(this));
}Public Function CanShip(rules As IEnumerable(Of IShipmentReadinessRule)) As Boolean
Return rules.All(Function(rule) rule.IsSatisfiedBy(Me))
End FunctionMit diesem Void-Accept-Muster können Sie mehrere Domänenregeln dynamisch auswerten. Wenn Sie Regeln in der Konfiguration speichern (insbesondere in einer mandantenfähigen Anwendung), können Sie zur Laufzeit eine neue Liste erstellen und sie an die Lieferung übergeben.
Derek weist darauf hin, dass sich bei mandantenfähigen Anwendungen das Regelwerk pro Kunde ändern kann. Sie könnten eine Liste von Richtlinien aus dem Speicher abrufen und sie dynamisch einfügen:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);Dim rules = LoadRulesForCustomer(customerId)
Dim canShip = shipment.CanShip(rules)Es wird gezeigt, wie dynamisches Dispatching und Entscheidungsfindung zur Laufzeit in Ihre Anwendung integriert werden können. Das gewünschte Verhalten wird durch das Ändern der Konfiguration, nicht des Modells, erreicht.
Gegen Ende geht Derek auf das Missverständnis ein, dass es schlecht ist, irgendetwas in eine Domain zu injizieren. Er betont, dass es darauf ankommt, was man einspeist. Das Injizieren von Domänenverhalten, wie Spezifikationen oder Richtlinien, ist nicht dasselbe wie das Injizieren von Infrastruktur.
Das Domänenmodell bleibt der Einstiegspunkt. Es ist für die Entscheidung zuständig, kann diese aber an andere Objekte delegieren, solange sie sich im selben Domänenkontext befinden.
Abschließend fordert Derek uns auf, kritisch zu denken: Haben Sie keine Angst vor void visit, public virtual void accept oder ähnlichen Mustern, wenn sie Klarheit und Wartbarkeit bringen. Wenn Sie Geschäftslogik auf kontrollierte Weise einfügen, gewinnen Sie an Flexibilität und Präzision.
Ganz gleich, ob Sie an einer neuen Klasse arbeiten oder eine bestehende Codebasis refaktorisieren, Double Dispatch C# bietet Ihnen eine saubere Möglichkeit, Anliegen zu trennen und gleichzeitig den Domänenfokus beizubehalten.
Wenn Sie Richtlinien, Spezifikationen oder sogar ein Besuchermuster verwenden, ohne sich dessen bewusst zu sein, sind Sie bereits kurz davor, Double Dispatch zu implementieren. Wenn man sie versteht, hat man mehr Kontrolle darüber, wie sich der Code zur Laufzeit verhält, was die Testbarkeit und Anpassungsfähigkeit verbessert.
Zusammenfassend lässt sich sagen, dass Double Dispatch in C# eine elegante und praktische Lösung sein kann, um Domänenlogik zu integrieren, Kapselung zu bewahren und flexibles Verhalten zu unterstützen. In Verbindung mit Mustern wie Visitor, Multiple Dispatch und Dynamic Keyword Usage (vorsichtig) ermöglicht es das Schreiben ausdrucksstarker, robuster Domänenmodelle.
Wenn Sie also das nächste Mal shipment.IsLate(policy) aufrufen, sollten Sie wissen, dass Sie damit ein grundlegendes Muster nutzen, das C# einem wirklich polymorphen Design näher bringt.
Beispiel-Tipp: Wenn Sie eine PurchaseOrder-Klasse erstellen und bestimmen möchten, ob ein Artikel auf der Grundlage einer Richtlinie hinzugefügt werden kann, übergeben Sie die Richtlinie an die PurchaseOrder und lassen Sie die Richtlinie die Bestellung besuchen. Das ist doppelter Versand in Aktion.
Sehen Sie sich das vollständige Video auf seinem YouTube Channel an und gewinnen Sie weitere Einblicke in das Thema.
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!
Join our newsletter, you’ll get exclusive access on article updates. We value your privacy