至少30年以前,一些软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans 将其定义为领域驱动设计(Domain-Driven Design,简称DDD)。在互联网开发“小步快跑,迭代试错” 的大环境下,DDD似乎是一种比较“古老而缓慢”的思想。然而,由于互联网公司也逐渐深入实体经济, 业务日益复杂,我们在开发中也越来越多地遇到传统行业软件开发中所面临的问题。本文就先来讲一下这些问题, 然后再尝试在实践中用DDD的思想来解决这些问题。
传统开发模式弊端
过度耦合
单体应用中,所有业务单元都在同一个项目中,各业务逻辑通过接口直接调用,呈星状调用链。
贫血症和失忆症
贫血领域对象(Anemic Domain Object)
是指仅用作数据载体,而没有行为和动作的领域对象。 简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态 会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。
更好的是采用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。 各类具备明确的职责划分,将领域逻辑分散到领域对象中。
DDD 优势
与微服务架构相得益彰
我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求, 可以将该限界上下文理解为一个微服务进程。
在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。 技术维度是类似MVC这样,业务维度则是指按业务领域来划分系统。
微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。 如果两者在追求的目标(业务维度)达到了上下文的统一,那么在具体做法上有什么联系和不同呢?
我们将架构设计活动精简为以下三个层面:
- 业务架构 —— 根据业务需求设计业务模块及其关系
- 系统架构 —— 设计系统和子系统的模块
- 技术架构 —— 决定采用的技术及框架
DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。 而微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦, 可以自由地选择合适的技术架构,去中心化地治理技术和数据。
战略建模
战略和战术设计是站在DDD的角度进行划分。战略设计侧重于高层次、宏观上去划分和集成限界上下文, 而战术设计则关注更具体使用建模工具来细化上下文。
领域
现实世界中,领域包含了问题域和解系统。一般认为软件是对现实世界的部分模拟。在DDD中, 解系统可以映射为一个个限界上下文,限界上下文就是软件对于问题域的一个特定的、有限的解决方案。
限界上下文
一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念, 包括它的属性和操作,都具有特殊的含义。
一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要通过显示边界进行通信。 系统通过确定的限界上下文来进行解耦,而每一个上下文内部紧密组织,职责明确,具有较高的内聚性。
一个很形象的隐喻:细胞质所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外, 并且确定了什么物质可以通过细胞膜。
如何划分限界上下文
划分限界上下文,不管是Eric Evans还是Vaughn Vernon,在他们的大作里都没有怎么提及。 显然我们不应该按技术架构或者开发任务来创建限界上下文,应该按照语义的边界来考虑。
我们的实践是,考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系; 或者从需求里提取一些动词,观察动词和对象之间的关系;我们将紧耦合的各自圈在一起,观察他们内在的联系, 从而形成对应的界限上下文。形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、 准确、简洁和完整。简言之,限界上下文应该从需求出发,按领域划分。
战术建模——细化上下文
实体
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。
例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的, 且其具有唯一标识(如公安系统分发的身份证号码)。
值对象
当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。
例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”} 这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。
值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实体。使用值对象, 可以更好地做系统优化、精简设计。它具有不变性、相等性和可替换性。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时, 会出现模型概念的公用,如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时 商品信息快照,那么将商品对象视为值对象是很好的选择。
聚合根
Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root) 是这个聚合的根节点。
聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值, 可以指导详细设计。聚合由根实体,值对象和实体组成。
领域服务
一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。
当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。 如原本由聚合根暴露的业务逻辑也需要依托于领域服务。
领域事件
领域事件是对领域内发生的活动进行的建模。
比如,抽奖平台的核心上下文是抽奖上下文,接下来介绍下我们对抽奖上下文的建模。
在抽奖上下文中,我们通过抽奖(DrawLottery)这个聚合根来控制抽奖行为,可以看到, 一个抽奖包括了抽奖ID(LotteryId)以及多个奖池(AwardPool),而一个奖池针对一个特定的 用户群体(UserGroup)设置了多个奖品(Award)。
另外,在抽奖领域中,我们还会使用抽奖结果(SendResult)作为输出信息,使用用户领奖记录 (UserLotteryLog)作为领奖凭据和存根。
业务领域分析法
- 收集用例
- 收集名词、形容词
- 根据词性建立模型和属性
- 根据名词定义、验证和完善属性
- 收集动词
- 根据动词确定模型关系
参考资料
- 领域驱动设计
- 领域驱动设计在互联网业务开发中的实践
- 业务中台设计方法论(DDD领域模型设计) —— 阿里云业务中台高级架构师:焦先
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://adolphor.github.io/2021/10/03/02-micro-service-ddd/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)