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
在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 Function IsLate(expectedDelivery As DateTime) As Boolean
Return _systemClock.Now() > expectedDelivery
End FunctionPublic Function IsLate(expectedDelivery As DateTime) As Boolean
Return _systemClock.Now() > expectedDelivery
End Function這現有的程式碼簡單但不靈活。 它依賴於編譯時的決定,並且將行為緊密結合到類中。 更改規則需要更改領域模型,並且測試更加困難,因為它取決於時間。
Derek引入介面IDeliveryTimingPolicy:
public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}public interface IDeliveryTimingPolicy
{
bool IsLate(Shipment shipment);
}Public Interface IDeliveryTimingPolicy
Function IsLate(shipment As Shipment) As Boolean
End InterfacePublic Interface IDeliveryTimingPolicy
Function IsLate(shipment As Shipment) As Boolean
End Interface然後他建立了兩個實現:
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 Function IsLate(shipment As Shipment) As Boolean
Return _clock.Now() > shipment.DeliveryDate.AddMinutes(30)
End FunctionPublic Function IsLate(shipment As Shipment) As Boolean
Return _clock.Now() > shipment.DeliveryDate.AddMinutes(30)
End Function現在在運輸類中,我們使用雙重調度:
public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}public bool IsLate(IDeliveryTimingPolicy policy)
{
return policy.IsLate(this);
}Public Function IsLate(ByVal policy As IDeliveryTimingPolicy) As Boolean
Return policy.IsLate(Me)
End FunctionPublic Function IsLate(ByVal policy As IDeliveryTimingPolicy) As Boolean
Return policy.IsLate(Me)
End Function這是經典的雙重調度:第一次調度是對運輸調用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 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 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 buffer這展示了如何在不更改領域模型的情況下改變運行時行為。 您可以根據政策的動態性質獲得多態行為,同時保持領域完整性。
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);
}
}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 ClassPublic 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 Class在這裡,Accept()和Visit()方法在兩種型別之間協調——物件和訪客——就像Derek使用的運輸和IDeliveryTimingPolicy一樣。 這種模式甚至在像C#這樣的單一調度語言中,也有助於實現多重調度行為。
在接下來的另一個例子中,Derek展示了如何通過集合評估多個規則。 他引入了如下規則:
HasValidDestinationRule
AllPackagesPackedRule
NotAlreadyShippedRule
每個都實現了IShipmentReadinessRule:
public bool IsSatisfiedBy(Shipment shipment)public bool IsSatisfiedBy(Shipment shipment)Public Function IsSatisfiedBy(shipment As Shipment) As BooleanPublic Function IsSatisfiedBy(shipment As Shipment) As Boolean然後,運輸類這樣評估它們:
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));
}Public Function CanShip(rules As IEnumerable(Of IShipmentReadinessRule)) As Boolean
Return rules.All(Function(rule) rule.IsSatisfiedBy(Me))
End FunctionPublic Function CanShip(rules As IEnumerable(Of IShipmentReadinessRule)) As Boolean
Return rules.All(Function(rule) rule.IsSatisfiedBy(Me))
End Function這種可接受的空模式允許動態評估多個領域規則。 如果您將規則儲存在配置中(尤其是在多租戶應用中),可以在運行時建立一個新列表並將其傳遞給運輸。
Derek指出,在多租戶應用中,規則集可能會因為客戶而改變。 您可以從儲存中獲取政策列表並動態注入它們:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);Dim rules = LoadRulesForCustomer(customerId)
Dim canShip = shipment.CanShip(rules)Dim rules = LoadRulesForCustomer(customerId)
Dim canShip = shipment.CanShip(rules)這展示了如何將動態調度和運行時決策層次化到應用中。 所需行為是通過改變配置,而不是模型來實現的。
最後,Derek闡述了一個誤解,即將任何事物注入領域都是壞的。 他強調的是注入了什麼才是重要的。 注入領域行為,如規範或政策,與注入基礎設施不同。
領域模型仍然是入口點。 它擁有決策權,但可以將其委派給其他物件,只要它們在同一個領域上下文中。
Derek最後敦促我們要批判性地思考:當它們帶來清晰度和可維護性時,不要害怕使用空訪客、公用虛擬空接收或類似模式。 當您以一種受控的方式注入業務邏輯時,您可以獲得靈活性和精確性。
因此,無論您是在製作一個新的類還是重構現有的程式碼庫,雙重調度C#為您提供了一種清晰的方法來分離關注點,同時保持對領域的專注。
如果您正在使用政策、規範,甚至毫無察覺地構建訪客模式,那麼您已經接近於實現雙重調度了。 理解它讓您更好地掌控程式碼在運行時的行為,提高可測試性和適應性。
總之:C#中的雙重調度可以是一個優雅且實用的解決方案,用於注入領域邏輯、保持封裝性和支持靈活的行為。 當與訪客、多重調度、動態關鍵字使用(謹慎)等模式結合使用時,能夠撰寫表達力強大、健全的領域模型。
所以,下次您調用shipment.IsLate(policy)時,請意識到您正在利用一種基本的模式,將C#推向真正的多態設計。
範例提示:如果您正在建立一個PurchaseOrder類,並希望根據政策確定能否新增一個項目,嘗試將政策傳遞給PurchaseOrder,讓政策存取訂單。 這就是雙重調度正在發揮作用。
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