大纲
1、什么是DDD
1.1、DDD的描述
- 全称 Domain Driven Design(领域驱动设计)。
- 一套应对复杂软件系统分析和设计的面向对象建模方法论。
- 微服务拆分,重构。
总结一下:
DDD作为一种战略思想可用于指导微服务的拆分,DDD不是一个软件架构,而是一种指导思想.
1.2、普遍的需求开发方式
举个例子,比如我们每一次需求的开发方式:
存在的问题:
- 用户的需求进行多次转化,可能失去最原始的意图。
- 研发的实现是从数据库开始的。也就是说那着数据库去匹配业务需求;如果需求变更是不是意味着表也会变更。如果下一期有新需求会不会这个表结构就不适用了。
- 现在的开发模式,就会导致业务逻辑会大量的存留在services层。
- 需求的边界也是比较模糊的,一旦考虑不全,就会对业务逻辑做出重大变更。
- 代码难以维护,程序员不考虑整体设计,对于这个系统,就会有一个一个的补丁。
- 代码中的if/else判断很多,因为需要考虑和涵盖的场景会多。
1.3、DDD的需求开发方式
这里列举下,由产品,测试,运营和开发人员共同参与的需求讨论的开发方式
这样做的好处:
- 消除信息的不对称。
- 产品与研发的讨论,往往不能够深刻的互相理解。初次的讨论可以在产品与运营,测试与研发之间进行。最后进行业务的大讨论。
- 领域模型的划分,就是对大的需求进行拆分。
- DDD倡导以业务为主,自顶向下的进行业务领域划分。
1.4、自顶向下的开发方式
1.4.1、现在的采用的架构模式
在这种架构中,往往采用的都是自下而上的开发方式,只有确定了数据库的结构,才能划分清晰的业务逻辑。开发的代码往往都是耦合在一起的。
1.4.2、DDD倡导的分层架构
- controller层更加纯粹,在原三层架构中,controller可能会聚合多个service层的调用。在新的分层架构中,则纯粹作为与前端或外部系统交互的数据转换层,不再包含业务逻辑。
- 应用层相比于service层,体现为更高级别的抽象,不在包含业务逻辑。
- 抽离原有的service层的逻辑,拆分在领域服务和领域对象中。
- 领域层为聚合和领域服务。其中聚合包括实体+值对象。这里有一个充血模型的概念。
1.4.3、贫血模型
说到充血模型,就要介绍下贫血模型。下面看一下大家日常开发过程中的实体类。
这里只是举个例子,大家可以看下我们的日常的开发中使用的实体类,大致也是类似的。针对与这样的实体类我们最对的操作是什么,就是生成get/set方法,toString方法等方法。其他的情形再没有了。大家可以思考下这样的实体类的价值在哪里,难倒只是给成员变量赋值么;如果只是赋值,那改成如下这种方式,不是更好,连get/set
方法可以是不写了。
总结一下:以上这种实体类便是贫血模型的典型体现。
什么是面向对象呢?
- 对象由属性、有行为。
- 更关注的是对象有哪些行为,通过对象的这些行为,我们可以使得一个事件完整执行。
- 贫血模型更像是拿着对象去做面向过程编程;将业务逻辑放在service层中的一个个很少复用的方法中。
###1.4.4、充血模型
对于一个实体对象,会有属性和行为,对于行为中,我们可能会封装一些业务逻辑,那么这种业务逻辑恰恰就是这个类的一个行为
这里结合我们的业务场景举个例子,在组织这个实体类中,是不是可以定义获取子组织,上级组织等一些行为呢。而不是在各个自服务去重复编码查询,这些查询往往耦合一定的业务逻辑,无法复用;后者除了作者自己,其他人无法知道有这样一个功能呢。
区别:
大多数业务逻辑放到 Domain 中,Service 是很薄的一层,封装少量业务逻辑,并且不和 DAO 打交道。
Service (事务封装) —> Domain Object <—> DAO
优点:
- 更加符合 OO 的原则
- Service 层很薄
缺点:
- 如何划分 Service 层逻辑和 Domain 层逻辑是非常含混的。
2、DDD的基本概念
这里就介绍下基本概念的理解,不介绍官方的语音定义了。
2.1、领域
领域就是一个确定的范围。重点是边界的确定。
域的核心思想是将问题逐级细分来减低业务和系统的复杂度。
##2.2、子域
领域可以进一步划分成子领域,即子域
##2.3、核心域
在领域划分过程中,会不断划分子域,子域按重要程度会被划分成三类:核心域、通用域、支撑域。
2.4、通用域
中间件服务或第三方服务。
被多个子域使用的通用功能就是通用域,没有太多企业特征,比如权限认证。
2.5、支撑域
企业公共服务。
对于功能来讲是必须存在的,但它不对产品核心竞争力产生影响,也不包含通用功能,有企业特征,不具有通用性,比如数据代码类的数字字典系统。
2.6、统一语言
统一概念。
可以在事件风暴(开会)中来统一语言,甚至是中英文的映射、业务与代码模型的映射等。可以使用一个表格来记录。
2.7、限界上下文
服务职责划分的边界。
领域模型存在边界之内。对于同一个概念,不同上下文会有不同的理解,比如商品,在销售阶段叫商品,在运输阶段就叫货品。
理论上,限界上下文的边界就是微服务的边界,因此,理解限界上下文在设计中非常重要。
2.8、聚合
聚合概念类似于你理解的包的概念,每个包里包含一类实体或者行为,它有助于分散系统复杂性,也是一种高层次的抽象,可以简化对领域模型的理解。
2.9、聚合根
一个上下文内可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根,一个聚合只有一个聚合根。
聚合的定义规则:
- 聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合。
- 聚合内的某些对象的状态必须满足某个业务规则。
- 一个聚合只有一个聚合根,聚合根是可以独立存在的,聚合中其他实体或值对象依赖与聚合根。
- 只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性。
2.10、实体
实体是具有身份和连贯性的领域概念。
文章可以是实体,作者也可以是,因为它们有 id 作为唯一标示。
2.11、值对象
为了更好地展示领域模型之间的关系,制定的一个对象,本质上也是一种实体,但相对实体而言,它没有状态和身份标识,它存在的目的就是为了表示一个值,通常使用值对象来传达数量的形式来表示。
3、需求开发中怎么应用DDD呢?
3.1、首先是需求的理解
在产品,测试,运营和研发之间进行需求的讨论与使用场景的分割。
3.2、包模块的划分
这里就拿一个文章管理系统举例子:
模块
一般尽量用一个模块(Module)来表示一个领域的限界上下文
一般的工程中包的组织方式为
{com.公司名.组织架构.业务.上下文.*}
,这样的组织结构能够明确地将一个上下文限定在包的内部。
import com.company.team.bussiness.counter.*;//计数上下文
import com.company.team.bussiness.category.*;//分类上下文
import com.company.team.bussiness.comment.*;//评论上下文
对于模块内的组织结构,一般情况下我们是按照领域对象、领域服务、领域资源库、适配层等组织方式定义的。
import com.company.team.bussiness.cms.domain.valobj.*;//领域对象-值对象
import com.company.team.bussiness.cms.domain.entity.*;//领域对象-实体
import com.company.team.bussiness.cms.domain.aggregate.*;//领域对象-聚合根
import com.company.team.bussiness.cms.service.*;//领域服务
import com.company.team.bussiness.cms.repo.*;//领域资源库
import com.company.team.bussiness.cms.facade.*;//领域适配层
领域对象
在文章领域对象中,我们需要定义个一个方法,来获取文章的计数量,用于页面上显示。我们可以定义聚合根(文章)和值对象(计数器);这里计数器之所以被列为值对象,而非实体的一个属性,是因为计数器是由多部分组成的,比如真实阅读量、推广阅读量等。
package com.company.team.bussiness.domain.aggregate;
import ...;
public class Article {
@Resource
private CategoryRepository categoryRepository;
private int articleId; //文章id
...
private ArticleCount articleCount; //文章计数器
//getter & setter
//查询计数显示数量,这里简化一些逻辑,甚至是不符合实际业务场景,这不重要,这里只为直观表达意思
public Integer getShowArticleCount() {
if(this.articleCount == null){
return 0;
}
return this.articleCount.realCount + categoryRepository.getCategoryWeight(this.category) + (this.articleCount.adCount * DayUtils.calDaysByNow(this.articleCount.deadDays));
}
}
可以看到,领域功能的内聚性更强,职责更加明确。
资源库
领域对象需要资源存储,资源库可以理解成 DAO,但它比 DAO 更宽泛,存储的手段可以是多样化的,常见的无非是数据库、分布式缓存、本地缓存等。资源库(Repository)的作用,就是对领域的存储和访问进行统一管理的对象。
import com.company.team.bussiness.repo.dao.ArticleDao;//数据库访问对象-文章
import com.company.team.bussiness.repo.dao.CommentDao;//数据库访问对象-评论
import com.company.team.bussiness.repo.dao.po.ArticlePO;//数据库持久化对象-文章
import com.company.team.bussiness.repo.dao.po.CommentPO;//数据库持久化对象-评论
import com.company.team.bussiness.repo.cache.ArticleObj;//分布式缓存访问对象-文章缓存访问
package com.company.team.bussiness.repo;
import ...;
@Repository
public class ArticleRepository {
@Autowired
private ArticleDao articleDao;
@AutoWired
private articleDaoCacheAccessObj articleCacheAccessObj;
public Article getArticleById(int articleId) {
Article article = articleCacheAccessObj.get(articleId);
if(article!=null){
return article;
}
article = getArticleFromDB(articleId);
articleCacheAccessObj.add(articleId, article);
return article;
}
private Article getArticleFromDB(int articleId) {...}
}
比起以往将资源管理放在服务中的做法,由资源库对资源进行管理,职责更加明确,代码的可读性和可维护性也更强。
适配层
在一个上下文中,有时需要对外部上下文进行访问,通常会引入适配层的概念来对外部上下文的访问进行一次转义。
package com.company.team.bussiness.facade;
import ...;
@Component
public class ArticleFacade {
@Resource
private ArticleService articleService;
public Article getArticle(ArticleContext context) {
ArticleResponse resp = articleService.getArticle(context.getArticleId());
return buildArticle(resp);
}
private Article buildArticle(ArticleResponse resp) {...}
}
总结
上文中,我们将领域行为封装到领域对象中,将资源管理行为封装到资源库中,将外部上下文的交互行为封装到适配层中。此时,我们再回过头来看领域服务时,能够发现领域服务本身所承载的职责也就更加清晰了,即就是通过串联领域对象、资源库和适配层等一系列领域内的对象的行为,对其他上下文提供交互的接口。