使用双分派解决领域实体和外部机制通信问题
经典DDD分层中领域层领域实体自身高内聚,在领域层内通过聚合,实体,事件和仓储接口完成领域业务逻辑。
最近在实际项目中遇到一种场景,有如下所示实体Device:
public class Device
{
void ExecuteCmd(string cmd)
{
//Do something "Domian" related before sending..
//Send command to real device via network
}
}
其中ExecuteCmd方法需要首先将指令通过网络发送到实际设备。由于领域实体自身不能完成诸如Email,网络操作等实际业务功能,故该功能只能通过某种服务完成。但是执行这个动作和其结果,流程却属于领域部分,故不能全部交由应用层来实现,否则将导致部分业务逻辑从领域层泄露。
为了解决这个问题,先后尝试了三种方式:
- 使用ServiceLocate模式向领域实体中注入服务接口
- 使用领域事件,在事件处理器重注入服务接口
- 使用双分派模式,传入执行接口
1实体中注入服务接口
public Interface IDeviceService
{
void Execute(string cmd);
}
public class Device
{
private IDeviceAgent _agent = ServiceLocator.GetInstance<IDeviceService>();
void ExecuteCmd(string cmd)
{
//Do something "Domian" related before sending..
//invoke agent
_agent.Execute(cmd);
}
}
该模式最简单,但却是DDD中的反模式,造成的后果是使得实体不再“纯净”,需要依赖一个服务接口,这样将给单元测试带来麻烦,并且容忍了这种方式之后,极有可能造成后续将Repository接口等注入实体,使得领域实体进一步腐化。
2使用领域事件
该方式下在需要定义领域事件,而在事件处理器中注入服务接口。此方法符合DDD要求,但在经典DDD分层项目中显得比较繁琐,需要频繁定义细粒度事件及事件处理器。并且调用依赖领域服务总线实现,总线消息实现为延迟传递则会带来一定问题。同样的,在这种方式下,单元测试也有一定的麻烦。
3使用双分派模式传入领域服务接口
Chassaging在这篇文章
If I have mail message entity and I want a Send method on it ?
Tiding your entity to the service that will use it is not conceptually satisfying. The
server >that will send the message is a medium and is not part of the entity itself.
It’s seems better to call server.Send(message).
The problem is that you can end breaking the tell don’t ask principle because the Send
method will ask the message for each property value. And you can be tempted to put
computations that should be in the message entity inside the Send method.
Let’s call Double Dispatch to the rescue !
- Design a server interface presenting a Send method that take the parameters it need (title, recipients, message body⋯)
- Add a SendThrough method to your entity that takes the server interface as parameter
- Implement SendTrhough so that it computes values to pass to the Send method.
That’s it.
简单来说,就是定义一个服务接口及其所需的参数,然后在实体中添加一个ExecuteThrough方法,将参数和接口传入,在基础层等实现该接口:
public class Device
{
public void ExecuteCmdThrough(string cmd, IDeviceService service)
{
//Do something "Domian" related before sending..
//Dispatch behaviors to interface
service.Execute(cmd);
}
}