C# 中的雙重調度:什麼時候注入依賴關係有意義—通過 Derek Comartin 的領域驅動設計範例解釋
在C#程式語言的領域中,雙重分派常常被誤解或未被充分利用。 這是一項強大的技術,可以在涉及的兩個對象之間啟用多態行為,特別是在處理派生類的行為時。
在他的影片"在DDD中的雙重分派:何時注入相依性才有意義"中,Derek Comartin 詳細說明雙重分派如何完美契合領域驅動設計(DDD)。 我們將深入探討他的例子,展示如何降低維護負擔,簡化現有代碼,甚至模擬其他語言中常見的訪問者模式。
如果您曾經面對單重分派在C#中的限制,或嘗試根據對象的實例和注入的策略或規則來確定行為,那麼這篇文章將幫助您釐清如何有效地使用雙重分派C#。
為什麼在領域模型中注入行為是有意義的
Derek首先指出DDD中的一個常見教條—您的領域模型應該不依賴於任何相依性。 但這並不總是實用或有用的。 在建模行為時,您可能需要注入邏輯,例如規則、政策或策略。 這就是雙重分派發揮作用的地方:您可以將領域概念(例如政策)傳遞到領域對象中,並讓基於方法的評估在外部處理—然而領域對象仍具有最終控制權。
當您考慮到您要耦合的是什麼:領域行為,而不是基礎結構時,這種模式就説得通。
不好的例子:硬編碼邏輯在領域中
為了展示問題,Derek以含有硬編碼邏輯來確定晚點的Shipment類開始。
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
這些類接收Shipment並根據其規則返回布林值。 以下是一段來自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);
}
現在在Shipment類中,我們使用雙重分派:
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}
這是經典的雙重分派:首次分派是調用IsLate()於Shipment並傳遞政策; 第二次分派是調用政策的IsLate(),以Shipment為參數。 涉及兩個對象,每個都參與確定行為—這是單重分派所無法實現的。
在測試和靈活性中的好處
這種方法創建了高度可測試和確定性代碼。 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 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
這展示了如何在不改變領域模型的情況下改變運行時行為。 您基於政策的動態性獲得多態行為,同時維持領域完整性。
在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對Shipment和IDeliveryTimingPolicy的使用一樣。 這個模式有助於即使在像C#這樣的單分派語言中實現多重分派行為。
使用政策集合構建規則
在另一個以下的例子中,Derek展示了如何通過集合評估多個規則。 他介紹了以下規則:
-
HasValidDestinationRule
-
AllPackagesPackedRule
- NotAlreadyShippedRule
每個都用IShipmentReadinessRule實現:
public bool IsSatisfiedBy(Shipment shipment)
public bool IsSatisfiedBy(Shipment 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));
}
這種void accept模式允許您動態地評估多個領域規則。 如果您在配置中存儲規則(特別是在多租戶應用程式中),您可以在運行時創建新列表並將其傳遞給Shipment。
在多租戶應用程式中使用動態規則
Derek指出在多租戶應用程式中,規則集可能因客戶而異。 您可以從存儲中獲取政策列表並動態注入它們:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
這顯示了如何將動態分派和運行時決策層疊進應用程序中。 所需行為是通過更改配置而非模型來實現的。
釐清誤解:並非所有相依性都是不好的
接近結尾時,Derek討論了將任何東西注入領域是壞事的誤解。 他強調重要的是您注入的是什麼。 注入領域行為,例如規範或政策,與注入基礎結構不同。
領域模型仍然是進入點。 它擁有決策,但可以將其委派給其他對象—只要它們在同一領域上下文中。
總結:為什麼雙重分派C#在DDD中強大
Derek最後提醒我們要批判性思考:當void visit、public virtual void accept或類似模式能帶來清晰性和可維護性時,不要害怕使用它們。 當您以受控方式注入業務邏輯時,您獲得靈活性和精確性。
因此,無論您是在新類上工作還是在重構現有代碼基,雙重分派C#為您提供了在維持領域聚焦的同時分離關注點的清晰方法。
如果您正在使用政策、規範,甚至在不知不覺中建立訪問者模式,您已經接近實施雙重分派。 了解它讓您對代碼在運行時的行為有更多控制力,提高可測試性和適應性。
結論
總而言之:在C#中雙重分派可以是一個優雅且實踐的解決方案,來注入領域邏輯、維護封裝性及支持靈活行為。 當與訪問者、重分派和動態關鍵字使用(謹慎地)一起使用時,它可以幫助書寫具表現力且強大的領域模型。
所以下次您調用shipment.IsLate(policy)時—請知道您正在利用一個將C#拉得更接近真正多態設計的基本模式。
示例提示:如果您正在創建一個PurchaseOrder類,並希望根據一個政策確定是否可以新增Item,請嘗試將政策傳遞給PurchaseOrder—讓政策訪問該訂單。 這就是雙重分派的實際應用。
