分层架构
DDD系统的传统分层架构:
分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。分层架构也分为几种:在严格分层架构中,某层只能与直接位于其下方的层发生耦合;而松散分层架构则允许任意上方层与任意下方层发生耦合。由于用户界面层和应用服务通常需要与基础设施打交道,许多系统都是基于松散分层架构的。
事实上,较低层也是可以和较高层发生耦合的,但这只局限于采用观察者模式或者调停者模式的情况。较低层是绝对不能直接访问较高层的。例如,在使用调停者模式时,较高层可能实现了较低层定义的接口,然后将实现对象作为参数传递到较低层。当较低层调用该实现时,它并不知道实现出自何处。
用户界面只用于处理用户显示和用户请求,它不应该包含领域或业务逻辑。有人(我就属于这种人)可能会认为,既然用户界面需要对用户输入进行验证,那么它就应该包含业务逻辑。事实上,用户界面所进行的验证和对领域模型的验证是不同的。在介绍实体的章节中将会就讲到,对于那些粗制的,并且只面向领域模型的验证行为,我们依然应该予以限制。
如果用户界面使用了领域模型中的对象,那么此时的领域对象仅限于数据的渲染展现。在采用这种方式时,可以使用展现模型(是不是视图模型呢?)对用户界面与领域对象进行解耦。
由于用户可能是人,也可能是其他的系统,有时用户界面层将采用开放主机服务(对接微信接口时是不是就可以用这种方式?)的方式向外提供API。
用户界面层是应用层的直接客户。
应用服务位于应用层中。应用服务和领域服务是不同的,因此领域逻辑也不应该出现在应用服务中。应用服务可以用于控制持久化事务和安全认证,或者向其他系统发送基于事件的消息通知,另外还可以用于创建邮件以发送给用户。应用服务本身并不处理业务逻辑,但它却是领域模型的直接客户。应用服务是很轻量的,它主要用于协调对领域对象的操作,比如聚合。同时,应用服务是表达用例和用户故事的主要手段。因此,应用服务的通常用途是:接收来自用户界面的输入参数,再通过资源库获取到聚合实例,然后执行相应的命令操作,比如:
public void CommitBacklogItemToSprint(string aTenantId, string aBacklogItemId, string aSprintId)
{
using(TransactionScope scope = new TransactioScope())
{
TenantId tenantId = new TenantId(aTenantId);
BacklogItem backlogItem = backlogItemRepository.backlogItemOfId(tenantId, new BacklogItemId(aBacklogItemId));
Sprint sprint = sprintRepository.sprintOfId(tenantId, new SprintId(aSprintId));
backlogItem.CommitTo(sprint);
scope.Complete();
}
}
如果应用服务比上述功能复杂许多,这通常意味着领域逻辑已经渗透到应用服务中了,此时的领域模型将变成贫血模型。因此,最佳实践是将应用层做成很薄的一层。当需要创建新的聚合时,应用服务应该使用工厂或聚合的构造函数来实例化对象,然后采用资源库对其进行持久化。应用服务还可以调用领域服务来完成和领域相关的任务操作,但此时的操作应该是无状态的。
当领域模型用于发布领域事件时,应用层可以将订阅方注册到任意数量的事件上,这样的好处是可以对事件进行存储和转发。同时,领域模型只需要关注自己的核心逻辑;领域事件发布器也可以保持轻量化,而不用依赖于消息机制的基础设施。
我们将在另外的章节中就讲到领域模型对业务逻辑的处理。然而,在传统的分层架构中,却存在着一些与领域相关的挑战。在分层架构中,领域层或多或少地需要使用基础设施层。这里我并不是说核心的领域对象会直接参与其中,而是说领域层中的有些接口实现依赖于基础设施层。
比如,资源库接口的实现需要基础设施层提供的持久化机制。那么,如果我们将资源库接口直接实现在基础设施层会怎样呢?由于基础设施层位于领域层之下,从基础设施层向上引用领域层则违反了分层架构的原则。遵从分层架构原则并不意味着领域对象需要与基础设施层发生直接耦合,此时,采用依赖倒置原则(推荐)可以为它们解耦。
在传统的分层架构中,基础设施层位于底层,持久化和消息机制便位于该层中。这里的消息包含了消息中间件所发的消息、基本的电子邮件或文本消息。可以将基础设施层中所有的组件和框架看作是应用程序的低层服务,较高层依赖于该层发生耦合以重用技术上的基础设施。即便如此,我们依然应该避免核心的领域模型对象与基础设施层发生直接耦合。