接到一项业务,我们首先下意识要解决这些问题,而为了解决业务问题,需要处理用户输入,处理业务逻辑,访问数据库,进行网络通信,向用户显示信息等。
而其中处理业务逻辑,是我们最需要关注的事情。
由于我们是领域驱动设计,而不是数据表驱动设计,所以我们优先考虑某场景下实体可能有哪些业务行为,而不是实体有哪些属性,也即根据用例流来设计软件。
一、分包示例
如何分包以及组织工程结构?
在DDD的战略设计中,我们关注于从一个宏观的视角俯视整个软件系统,然后通过一定的原则对系统进行子域和限界上下文的划分,即通过软件所实现的业务功能进行模块化划分。
在DDD中,聚合根是主要业务逻辑的承载体,也是“内聚性”原则的典型代表,因此通常的做法便是基于聚合根进行顶层包的划分。
以常见的用户登录网站为例,涉及到的聚合根显然是用户,可分为User包。
DDD将系统分为四种不同的层:
- 用户界面层(UI) 负责向用户显示信息和解释用户指令,用户不仅仅代表使用界面的人
- 应用服务层(application) 定义软件要完成的任务,协调下一层领域对象的工作
- 应用服务实现类(serviceImpl)
- 事件处理器(handler)
- 领域层(domain) 负责表达业务概念、业务状态信息以及业务规则
- 模型(model)
- 资源库(repository)
- 基础设施层(infrastructure)为上面各层提供通用的技术能力,包括持久化,传递消息,绘制界面等
- 领域服务 (service)
- 仓储实现 (persistence)
- 防腐层实现 (adapter)
上层可以调用或依赖下层,下层不能依赖上层。
1.简单分包
通常情况,目录组织如下:
├── order
├── OrderApplicationService.java
├── OrderController.java
├── OrderPaymentProxy.java
├── OrderPaymentService.java
├── OrderRepository.java
├── command //命令对象
├── ChangeAddressDetailCommand.java
├── CreateOrderCommand.java
├── OrderItemCommand.java
├── PayOrderCommand.java
└── UpdateProductCountCommand.java
├── exception
├── OrderCannotBeModifiedException.java
├── OrderNotFoundException.java
├── PaidPriceNotSameWithOrderPriceException.java
└── ProductNotInOrderException.java
├── model //领域模型
├── Order.java
├── OrderFactory.java
├── OrderId.java
├── OrderIdGenerator.java
├── OrderItem.java
└── OrderStatus.java
└── representation //展现层
├── OrderItemRepresentation.java
├── OrderRepresentation.java
└── OrderRepresentationService.java
2.复杂分包
若聚合根复杂程度更高,比如聚合根内聚其他聚合根,则以如下方式组织目录:
├── com.example.user //顶级模块名+限界上下文
├── resource //用户界面层
├── RoleController.java
└── UserController.java
├── application //应用层
├── command //命令对象
└── CreateUserCommand.java
├── representation //展现层
├── UserRepresentationService.java
└── UserSummaryRepresentation.java
└── UserApplicationService.java //应用服务
├── domain //领域层
└── model //领域模型
├── user //分包
├── UserId.java //值对象
└── User.java //聚合根
└── role //分包
├── RoleId.java //值对象
└── Role.java //聚合根
└── infrastructure //基础设施层
├── services //领域服务
└── MD5EncryptionService.java
└── persistence //持久化
└── UserRepository.java //资源库
注意,上面是复杂情况下的目录组建方式,是分清子领域后的再分层,包内各概念是紧密联系高内聚的。
3.错误分包
不要直接根据分层组织来组建目录,比如这样:
├── com.example.project
├── controller //用户界面层
├── OrderController.java
├── ArticleController.java
└── UserController.java
├── application //应用层
├── OrderApplicationService.java
├── ArticleApplicationService.java
└── UserApplicationService.java //应用服务
├── domain //领域层
└── model //领域模型
├── user //分包
├── UserId.java //值对象
└── User.java //聚合根
└── Order //分包
├── OrderId.java //值对象
└── Order.java //聚合根
└── infrastructure //基础设施层
├── UserRepository.java
└── OrderRepository.java
直接把同一领域的概念分散到四个层次中,每个模块都要乘以4,这样关注同一层次的每个类有哪些方法的情况很少出现,这种分包的方式减弱了领域模型的联系,很难跟踪同一概念。
二、分包注意事项
1.组合多个限界上下文的情况
当用户界面需要组合多个限界上下文时,甚至不属于自己领域的外来来源的领域对象时,分包就遇到了困难。
├── com.example.project
├── User //用户上下文
├── Order //订单上下文
├── Article //图文上下文
├── Product //产品上下文
└── ProductArticle //产品图文上下文
├── controller //控制器
└── application //应用层
这时,可以思考这种情况是否产生了新的领域上下文?
可以另外新建组合包,同时只包含应用层,不包含领域层。
若认为产生了新的领域,则添加领域层。
2.根据聚合进行分包
聚合是边界,聚合根则是对外交互的唯一通道,理应承担整个聚合的实例化工作。若要严格控制聚合的生命周期,可以禁止任何外部对象绕开聚合根直接创建其内部的对象。
可以为每个聚合建立一个包(package),除聚合根之外,聚合内的其他实体和值对象的构造函数皆定义为默认访问修饰符。一个聚合一个包,位于包外的其他类就无法访问这些对象的构造函数。
//在问题的上下文中
package com.example.project.questionContext.domain.model;
//公共访问修饰符,对外交互的唯一通道
public class Question {
public Question(String title,String description){...}
}
//与问题的上下文在同一个包中
package com.example.project.questionContext.domain.model;
//默认访问修饰符,只允许同一个包中的类访问
public class Answer{
Answer(String result){...}
}
目录:
领域驱动设计实现疑难解答(二):如何建模
领域驱动设计实现疑难解答(三):如何处理关联
领域驱动设计实现疑难解答(四):如何渲染数据
领域驱动设计实现疑难解答(五):如何发布领域事件