Zum Fußzeileninhalt springen
Iron Academy Logo
Lernen Sie C#
Lernen Sie C#

Andere Kategorien

Double Dispatch C#: Wann das Injizieren von Abhängigkeiten sinnvoll ist - erklärt am Beispiel von Derek Comartin's Domain-Driven Design

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.

Warum die Injektion von Verhalten in Domänenmodelle sinnvoll ist

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.

Das schlechte Beispiel: Hart kodierte Logik in der Domäne

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 bool IsLate(DateTime expectedDelivery)
{
    return _systemClock.Now() > expectedDelivery;
}

Der 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.

Der Refactor: Verwendung von Policies und Double Dispatch

Derek stellt die Schnittstelle IDeliveryTimingPolicy vor:

public interface IDeliveryTimingPolicy
{
    bool IsLate(Shipment shipment);
}
public interface IDeliveryTimingPolicy
{
    bool IsLate(Shipment shipment);
}

Anschließend erstellt er zwei Implementierungen:

  1. Standardlieferzeitplan

  2. 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 bool IsLate(Shipment shipment)
{
    return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}

In der Klasse "Shipment" verwenden wir nun double dispatch:

public bool IsLate(IDeliveryTimingPolicy policy)
{
    return policy.IsLate(this);
}
public bool IsLate(IDeliveryTimingPolicy policy)
{
    return policy.IsLate(this);
}

Dies 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.

Vorteile beim Testen und Flexibilität

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 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

Dies 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.

Doppelter Versand vs. Besucher-Muster in C

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 abstract class Shape
{
    public abstract void Accept(IVisitor visitor);
}

public class Circle : Shape
{
    public override void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Hier 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#.

Regeln mit Sammlungen von Policies zusammenstellen

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.:

  • HasValidDestinationRule

  • AlleVerpakungenVollständigRegel

  • Regel "Nicht bereits versendet"

Jeder implementiert IShipmentReadinessRule mit:

public bool IsSatisfiedBy(Shipment shipment)
public bool IsSatisfiedBy(Shipment shipment)

Anschließend werden sie von der Klasse Shipment wie folgt ausgewertet:

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));
}

Mit 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.

Dynamische Regeln in mandantenfähigen Anwendungen

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);
var rules = LoadRulesForCustomer(customerId);
var 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 Änderung der Konfiguration, nicht des Modells erreicht.

Klären von Missverständnissen: Nicht alle Abhängigkeiten sind schlecht

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.

Zusammenfassung: Warum Double Dispatch C# in DDD leistungsfähig ist

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.

Abschluss

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.

Hero Worlddot related to Double Dispatch C#: Wann das Injizieren von Abhängigkeiten sinnvoll ist - erklärt am Beispiel...
Hero Affiliate related to Double Dispatch C#: Wann das Injizieren von Abhängigkeiten sinnvoll ist - erklärt am Beispie...

Verdienen Sie mehr, indem Sie teilen, was Sie lieben

Erstellen Sie Inhalte für Entwickler, die mit .NET, C#, Java, Python oder Node.js arbeiten? Verwandeln Sie Ihr Fachwissen in ein zusätzliches Einkommen!

Iron Support Team

Wir sind 24 Stunden am Tag, 5 Tage die Woche online.
Chat
E-Mail
Rufen Sie mich an