双重调度 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);
}然后,他创建了两个实施方案:
1.标准交付时间政策
2.缓冲交付计时策略
这些类接收 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() 并传递策略; 第二个调度是在策略上调用 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 使用 Shipment 和 IDeliveryTimingPolicy 一样。 即使在 C# 这样的单一调度语言中,该模式也有助于实现多重调度行为。
使用策略集合组成规则
在下面的另一个示例中,Derek 演示了如何通过集合评估多个规则。 他介绍了以下规则
HasValidDestinationRule
所有软件包打包规则
- 尚未发货规则
每种实现 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));
}这种 void accept 模式允许您动态评估多个域规则。 如果您在配置中存储规则(尤其是在多租户应用程序中),您可以在运行时创建一个新列表并将其传递给装运。
多租户应用程序中的动态规则
Derek 指出,在多租户应用程序中,规则集可能会因客户而异。 您可以从存储中获取策略列表并动态注入:
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);这说明了如何将动态调度和运行时决策分层到您的应用程序中。 通过更改配置而不是模型,可以实现所需的行为。
澄清误解:并非所有依赖关系都是坏的
最后,德里克谈到了 "在域名中注入任何东西都是不好的 "这一误解。 他强调,重要的是您要注入什么。 注入领域行为(如规范或策略)与注入基础架构不同。
领域模型仍然是切入点。 它拥有决定权,但可以将其委托给其他对象--只要它们在同一领域上下文中。
总结:为什么双调度 C# 在 DDD 中功能强大
最后,Derek 敦促我们进行批判性思考:不要害怕 void visit、public virtual void accept 或类似模式,只要它们能带来清晰度和可维护性。 当您以可控的方式注入业务逻辑时,您将获得灵活性和精确性。
因此,无论您是在开发一个新的类,还是在重构现有的代码库,双调度 C# 都能为您提供一种简洁的方法来分离关注点,同时保持对领域的关注。
如果您正在使用策略、规范,甚至在不知不觉中构建了访问者模式,那么您已经接近实现双重调度了。 了解了这些工具,您就能更好地控制代码在运行时的表现,提高可测试性和适应性。
结论
总而言之:C# 中的双分派是一种优雅而实用的解决方案,可以注入领域逻辑、保持封装并支持灵活的行为。 当与访问者、多重调度和动态关键字使用(仔细)等模式一起使用时,它可以编写出表现力强、健壮的领域模型。
下一次,当您调用 shipment.IsLate(policy) 时,请知道您正在利用一种基本模式,它使 C# 更接近真正的多态设计。
示例提示:如果您正在创建一个 PurchaseOrder 类,并希望根据策略确定是否可以添加一个 Item,请尝试将策略传递给 PurchaseOrder,然后让策略访问订单。 这就是双重派遣的实际效果。

