Przejdź do treści stopki
Iron Academy Logo
Naucz się C#
Naucz się C#

Inne Kategorie

Podwójne Dispatch C#: Kiedy wstrzykiwanie zależności ma sens – wyjaśnione na przykładzie podejścia Domain-Driven Design Dereka Comartina

Derek Comartin
8m 44s

W zakresie języka programowania C#, podwójne wysyłanie jest często źle rozumiane lub niedostatecznie wykorzystywane. To potężna technika, która umożliwia zachowanie polimorficzne pomiędzy dwoma zaangażowanymi obiektami, zwłaszcza podczas obsługi zachowania w klasach pochodnych.

W swoim wideo "Double Dispatch in DDD: When Injecting Dependencies Makes Sense", Derek Comartin wyjaśnia, jak podwójne wysyłanie idealnie pasuje do Domain-Driven Design (DDD). Zgłębimy jego przykłady, pokazując, jak może zmniejszyć obciążenie konserwacyjne, uprościć istniejący kod, a nawet naśladować wzorce takie jak wzorzec gościa, często spotykane w innych językach.

Jeśli kiedykolwiek miałeś do czynienia z ograniczeniami pojedynczego wysyłania w C#, lub starałeś się ustalić zachowanie na podstawie zarówno instancji obiektu, jak i wstrzykniętej strategii lub reguły, ten artykuł pomoże wyjaśnić, jak skutecznie można używać podwójnego wysyłania w C#.

Dłączego wstrzykiwanie zachowania ma sens w modelach domenowych

Derek zaczyna od wskazania powszechnego dogmatu w DDD — że wasz model domenowy powinien być wolny od zależności. Ale to nie zawsze jest praktyczne lub użyteczne. Podczas modelowania zachowania, możemy potrzebować wstrzykiwać logikę, taką jak reguły, polityki lub strategie. To właśnie tutaj podwójne wysyłanie pomaga: można przekazać koncepcję domeny, np. politykę, do obiektu domeny i pozwolić, by ocena metod była obsługiwana zewnętrznie — a mimo to obiekt domeny ma ostateczną kontrolę.

Ten wzorzec ma sens, jeśli weźmiemy pod uwagę, do czego się łączymy: do zachowania domeny, a nie do infrastruktury.

Zły przykład: zakodowana logika w domenie

Aby zademonstrować problem, Derek zaczyna od klasy Shipment zawierającej zakodowaną logikę do określenia opóźnienia:

public bool IsLate(DateTime expectedDelivery)
{
    return _systemClock.Now() > expectedDelivery;
}
public bool IsLate(DateTime expectedDelivery)
{
    return _systemClock.Now() > expectedDelivery;
}

Ten istniejący kod jest prosty, ale niewielce elastyczny. Polega na decyzjach w czasie kompilacji i ściśle łączy zachowanie z klasą. Zmiana reguły wymagałaby zmiany modelu domeny, a testowanie jest trudniejsze, ponieważ jest zależne od czasu.

Refaktoryzacja: używanie polityk i podwójnego wysyłania

Derek wprowadza interfejs IDeliveryTimingPolicy:

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

Następnie tworzy dwie implementacje:

  1. StandardDeliveryTimingPolicy

  2. BufferedDeliveryTimingPolicy

Te klasy przyjmują Shipment i zwracają wartość logiczną na podstawie swojej reguły. Oto fragment kodu z 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);
}

Teraz w klasie Shipment używamy podwójnego wysyłania:

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

To klasyczne podwójne wysyłanie: pierwsze wysyłanie polega na wywołaniu IsLate() na przesyłce i przekazaniu polityki; drugie wysyłanie polega na wywołaniu IsLate() na polityce z przesyłką jako parametrem. Dwa obiekty są zaangażowane, każdy uczestniczy w określaniu zachowania - coś, czego pojedyncze wysyłanie nie jest w stanie osiągnąć.

Korzyści z testowania i elastyczność

To podejście prowadzi do bardzo testowalnego i deterministycznego kodu. Derek pokazuje przykłady, używając standardowych i buforowanych polityk, gdzie dane testowe są kontrolowane, a typ w czasie wykonania każdego obiektu decyduje o zachowaniu.

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

Pokazuje to, jak można zmienić zachowanie w czasie wykonania bez zmiany modelu domeny. Uzyskujesz polimorficzne zachowanie na podstawie dynamicznej natury polityki, jednocześnie utrzymując integralność domeny.

Podwójne wysyłanie kontra wzorzec wizytatora w C

Przykład Dereka przypomina wzorzec wizytatora, gdzie definiujesz zestaw operacji do przeprowadzenia na obiektach bez zmieniania ich struktury. Typowo, w wzorcu wizytatora, zobaczyłbyś coś takiego:

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

Tutaj, metody Accept() i Visit() są skoordynowane pomiędzy dwoma typami — obiektem i wizytatorem — podobnie jak użycie Shipment i IDeliveryTimingPolicy przez Dereka. Ten wzorzec pomaga zaimplementować zachowanie wielokrotnego wysyłania nawet w języku z pojedynczym wysyłaniem, takim jak C#.

Kompozycja reguł z kolekcjami polityk

W kolejnym przykładzie Derek pokazuje, jak można oceniać wiele reguł za pomocą kolekcji. Wprowadza reguły takie jak:

  • HasValidDestinationRule

  • AllPackagesPackedRule

  • NotAlreadyShippedRule

Każda implementuje IShipmentReadinessRule przy użyciu:

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

Następnie klasa Shipment ocenia je w ten sposób:

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

Ten wzorzec akceptacji pozwala ocenić dynamicznie wiele reguł domenowych. Jeśli przechowujesz reguły w konfiguracji (szczególnie w aplikacji dla wielu najemców), możesz stworzyć nową listę w czasie wykonania i przekazać ją przesyłce.

Dynamiczne reguły w aplikacjach multi-tenant

Derek zauważa, że w aplikacjach multi-tenant, zestaw reguł może się zmieniać dla każdego klienta. Możesz pobrać listę polityk z magazynu i wstrzyknąć je dynamicznie:

var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);

Pokazuje to, jak dynamiczne wysyłanie i podejmowanie decyzji w czasie wykonania można wdrożyć w aplikacji. Pożądane zachowanie osiąga się przez zmianę konfiguracji, a nie modelu.

Wyjaśniając nieporozumienia: nie wszystkie zależności są złe

Pod koniec Derek porusza błędne przekonanie, że wstrzykiwanie czegokolwiek do domeny jest złe. Podkreśla, że ważne jest co wstrzykujesz. Wstrzykiwanie zachowania domeny, jak specyfikacje lub polityki, nie jest tym samym co wstrzykiwanie infrastruktury.

Model domeny pozostaje punktem wejścia. Ma kontrolę nad decyzją, ale może ją delegować na inne obiekty — pod warunkiem, że są one w tym samym kontekście domenowym.

Podsumowanie: dłączego podwójne wysyłanie w C# jest potężne w DDD

Derek kończy, zachęcając nas do krytycznego myślenia: nie bójcie się void visit, public virtual void accept ani podobnych wzorców, gdy przynoszą one klarowność i łatwość w utrzymaniu. Kiedy wstrzykujesz logikę biznesową w kontrolowany sposób, zyskujesz elastyczność i precyzję.

Niezależnie od tego, czy pracujesz nad nową klasą, czy refaktoryzujesz istniejącą bazę kodu, podwójne wysyłanie w C# daje czysty sposób na oddzielenie odpowiedziąlności, z jednoczesnym zachowaniem skupienia na domenie.

Jeśli używasz polityk, specyfikacji, czy nawet budujesz wzorzec wizytatora, nawet tego nie zauważywszy, jesteś już blisko implementacji podwójnego wysyłania. Zrozumienie tego daje większą kontrolę nad tym, jak kod zachowuje się w czasie wykonywania, poprawiając testowalność i zdolność adaptacji.

Wnioski

Podsumowując: podwójne wysyłanie w C# może być eleganckim i praktycznym rozwiązaniem do wtryskiwania logiki domenowej, zachowania kapsułkowania i wspierania elastycznych zachowań. Gdy używane z wzorcami takimi jak wizytator, wielokrotne wysyłanie i użycie dynamicznego słowa kluczowego (z ostrożnością), umożliwia pisanie wyrazistych, potężnych modeli domenowych.

Więc następnym razem, gdy wywołasz shipment.IsLate(policy) - wiedz, że wykorzystujesz fundamentalny wzorzec, który przybliża C# do prawdziwie polimorficznego projektowania.

Porada: Jeśli tworzysz klasę PurchaseOrder i chcesz określić, czy można dodać element na podstawie polityki, spróbuj przekazać politykę do PurchaseOrder — i niech polityka odwiedzi zamówienie. To podwójne wysyłanie w działaniu.

Obejrzyj pełne wideo na jego kanale YouTube Channel i zdobywaj więcej informacji na ten temat.

Hero Worlddot related to Podwójne Dispatch C#: Kiedy wstrzykiwanie zależności ma sens – wyjaśnione na przykładzie...
Hero Affiliate related to Podwójne Dispatch C#: Kiedy wstrzykiwanie zależności ma sens – wyjaśnione na przykładzi...

Zarabiaj więcej, dzieląc się tym, co kochasz

Tworzysz treści dla deweloperów pracujących z .NET, C#, Java, Python, czy Node.js? Zamień swoją wiedzę specjalistyczną na dodatkowy dochód!

Zespol wsparcia Iron

Jestesmy online 24 godziny, 5 dni w tygodniu.
Czat
Email
Zadzwon do mnie