雙重分派C#:何時注入依賴是有意義的 — 通過Derek Comartin的領域驅動設計範例解析
在C#程式語言領域中,雙重調度經常被誤解或未充分利用。 這是一種強大的技術,能夠在涉及的兩個物件之間實現多態行為,特別是在處理派生類的行為時。
在他的视频"Double Dispatch in DDD: When Injecting Dependencies Makes Sense"中,Derek Comartin 解釋了雙重調度如何完美地融入領域驅動設計(DDD)中。 我們將深入探索他的例子,展示如何減少維護負擔、簡化現有程式碼,甚至模仿其他語言中常見的訪客模式。
如果您曾經面對C#中單一調度的限制,或試圖根據物件實例和引入的策略或規則來決定行為,這篇文章將幫助您釐清如何有效地使用C#的雙重調度。
為什麼在領域模型中引入行為是合理的
Derek首先指出在DDD中一個常見的教條——您的領域模型應該完全沒有依賴。 但這並不總是實用或有用的。 在建模行為時,您可能需要引入邏輯,如規則、政策或策略。 這就是雙重調度的用武之地:您可以將領域概念(如政策)傳遞到領域物件中,並讓基於方法的評估在外部處理——但領域物件仍然具有最終控制權。
當您考慮您所攜帶的東西時,此模式是合理的:領域行為,而不是基礎設施。
糟糕的例子:領域中的硬編程邏輯
為了展示問題,Derek以一個包含硬編程邏輯以確定晚到的運輸類開始:
public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}public bool IsLate(DateTime expectedDelivery)
{
return _systemClock.Now() > expectedDelivery;
}這現有的程式碼簡單但不靈活。 它依賴於編譯時的決定,並且將行為緊密結合到類中。 更改規則需要更改領域模型,並且測試更加困難,因為它取決於時間。
重構:使用政策和雙重調度
Derek引入介面IDeliveryTimingPolicy:
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}然後他建立了兩個實現:
StandardDeliveryTimingPolicy
- BufferedDeliveryTimingPolicy
這些類接受運輸並根據其規則返回布林值。 以下是來自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);
}現在在運輸類中,我們使用雙重調度:
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}這是經典的雙重調度:第一次調度是對運輸調用IsLate()並傳遞政策; 第二次調度是對政策調用IsLate(),並將運輸作為參數。 涉及兩個物件,每個物件都參與確定行為——這是單一調度無法做到的。
測試和靈活性的好處
這種方法導致高度可測試和確定性的程式碼。 Derek展示了使用標準和緩衝政策的例子,其中測試資料是可控的,每個物件的運行時型別決定行為。
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 buffervar 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這展示了如何在不更改領域模型的情況下改變運行時行為。 您可以根據政策的動態性質獲得多態行為,同時保持領域完整性。
C# 中的雙重調度與訪客模式
Derek的例子類似於訪客模式,其中您定義了一組操作以便在不改變結構的情況下對物件進行操作。 通常,在訪客模式中,您會看到類似的東西:
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);
}
}在這裡,Accept()和Visit()方法在兩種型別之間協調——物件和訪客——就像Derek使用的運輸和IDeliveryTimingPolicy一樣。 這種模式甚至在像C#這樣的單一調度語言中,也有助於實現多重調度行為。
通過政策集合組成規則
在接下來的另一個例子中,Derek展示了如何通過集合評估多個規則。 他引入了如下規則:
HasValidDestinationRule
AllPackagesPackedRule
- NotAlreadyShippedRule
每個都實現了IShipmentReadinessRule:
public bool IsSatisfiedBy(Shipment shipment)public bool IsSatisfiedBy(Shipment shipment)然後,運輸類這樣評估它們:
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));
}這種可接受的空模式允許動態評估多個領域規則。 如果您將規則儲存在配置中(尤其是在多租戶應用中),可以在運行時建立一個新列表並將其傳遞給運輸。
多租戶應用中的動態規則
Derek指出,在多租戶應用中,規則集可能會因為客戶而改變。 您可以從儲存中獲取政策列表並動態注入它們:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);這展示了如何將動態調度和運行時決策層次化到應用中。 所需行為是通過改變配置,而不是模型來實現的。
澄清誤解:並非所有依賴都是壞的
最後,Derek闡述了一個誤解,即將任何事物注入領域都是壞的。 他強調的是注入了什麼才是重要的。 注入領域行為,如規範或政策,與注入基礎設施不同。
領域模型仍然是入口點。 它擁有決策權,但可以將其委派給其他物件,只要它們在同一個領域上下文中。
總結:為什麼在DDD中雙重調度C#非常強大
Derek最後敦促我們要批判性地思考:當它們帶來清晰度和可維護性時,不要害怕使用空訪客、公用虛擬空接收或類似模式。 當您以一種受控的方式注入業務邏輯時,您可以獲得靈活性和精確性。
因此,無論您是在製作一個新的類還是重構現有的程式碼庫,雙重調度C#為您提供了一種清晰的方法來分離關注點,同時保持對領域的專注。
如果您正在使用政策、規範,甚至毫無察覺地構建訪客模式,那麼您已經接近於實現雙重調度了。 理解它讓您更好地掌控程式碼在運行時的行為,提高可測試性和適應性。
結論
總之:C#中的雙重調度可以是一個優雅且實用的解決方案,用於注入領域邏輯、保持封裝性和支持靈活的行為。 當與訪客、多重調度、動態關鍵字使用(謹慎)等模式結合使用時,能夠撰寫表達力強大、健全的領域模型。
所以,下次您調用shipment.IsLate(policy)時,請意識到您正在利用一種基本的模式,將C#推向真正的多態設計。
範例提示:如果您正在建立一個PurchaseOrder類,並希望根據政策確定能否新增一個項目,嘗試將政策傳遞給PurchaseOrder,讓政策存取訂單。 這就是雙重調度正在發揮作用。

