你需要知道DDD基本知识
- 创业
- 2025-08-18 20:57:04

0 概述
2004 年埃里克·埃文斯(Eric Evans)发表了《领域驱动设计》(Domain-Driven Design –Tackling Complexity in the Heart of Software)这本书,从此领域驱动设计(Domain Driven Design,简称 DDD)诞生;领域驱动设计这一理念迅速被行业采纳,时至今日仍是绝大多数人进行业务建模的首要方法。随着Martin Fowler 提出微服务架构[2],DDD也迎来了新的时代。 DDD 是一种架构设计的方法论;在具体落地的时候分为战略设计和战术设计,这两个阶段用于指导领域模型的设计和实现。DDD概念比较多比如:领域、子域、核心域、通用域、支撑域、限界上下文、聚合、聚合根、实体、值对象等等;这些名词,都是关键概念,但它们实在有些晦涩难懂,可能导致你还没开始实践 DDD 就打起了退堂鼓。因此,本文将结合自己工作理解对DDD相关概念总结。
1 战略设计战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
1.1 统一语言定义:统一语言(Ubiquitous Language)是一种业务方与技术方共同使用的共同语言,业务方与技术方通过共同语言描述业务规则与需求变动,共同语言为双方提供了协作沟通基础。这里的业务方泛指一切非最终软件实现者(如客户、产品、业务、BI、UED等)。早期共同语言形式:用户画像(User Persona)、用户旅程能够从流程角度有效地构成共同语言,数据字典(Data Dictionary)是从软件实现侧形成的共同语言。 问题:DDD是一种模型驱动设计方法,为什么不直接使用模型模型作为统一语言呢? 直接使用模型作为统一语言,其效果并不理想主要原因有:1)业务方难以直接把模型和他们对系统的理解关联到一起,这主要是因为业务方看待系统的角度和开发人员不同,业务部方大多数习惯从业务维度去感知系统,如流程、交互、功能、规则、价值等去描述软件系统。2)模型则偏重于数据角度,描述不同了业务维度下,数据如何改变,以及如何支撑对应的计算与统计。3)模型是从已知的需求中总结提炼的知识,模型无法表达未知需求中尚未提炼的知识。此外统一语言可以作为中间的隔离层、提供句够的缓冲来帮助反馈模型的不足,同时也更能满足人与人之间的交流的需求。 统一语言案例:(PS:统一语言形式不重要,核心是统一语言要和模型关联,且需要多方认可&承认并达成一致)。 统一语言包含哪些东西:
领域模型;源自领域模型的概念与逻辑。术语和概念;定义和约定一系列术语和概念,用于描述和表达领域中的事物、关系和行为。限界上下文;围绕领域模型设置的边界。规约和约束;对于领域模型和领域行为的规约和约束。 1.2 限界上下文定义:限界上下文(Bounded Context)是一个显示的边界,用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。可以拆解为了两次词来理解,即限界和上下文;限界就是领域的边界,上下文则是语义环境。 Eric Evans 用细胞来形容限界上下文,因为“细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”这里,细胞代表上下文,而细胞膜代表了包裹上下文的边界。
案例:我的一天建模,早上我乘坐地铁到公司上班,坐上地铁上我打开手机进入天猫好房喵屋会签到,然后我又打开微信读书读了会书并买了年卡,9点半之前到达公司开始工作。 不难看出下文文其实动态业务流程被边界(限界)静态切分的产物,所以产生了出行上下文、签到上下文等。每限界上下文提供不同业务能力,以满足当前上下文角色的目标;限界上下文划定领域知识的边界,形成了各自的知识语境。 案例:物流运输系统,该系统需要支持集装箱在铁路运输和公路运输的多式联运,需要计算每次多次联运的运费,以管理公司与委托公司的账目。 问题:系统定义运输上下文和财务上下文,运费计算是否可以放到财务上下文?如果从知识和能力的角度去理解,财务上下文的领域对象不具备计算运费的领域知识,不了解运输各种费用怎么计算的。缺乏这些知识自然就不具备计算运费的能力。如下图所示,财务上下文通过运输上下文,做账目结算,生成账单。限界上下文之间的复用体现对业务能力的复用而非对知识语境边界内的模型复用。 限界上下文特征:领域模型知识语境、业务能力纵向切分、自治的架构单元(最小完备、自我履行、独立进化、稳定空间)。 如下图所示,模块是从技术维度横向切分,再从领域维度进行纵向切分;限界上下文则是先从领域维度进行纵向切分,再从技术维度对限界上下文进行横向切分。 案例:供应链的商品模型,采购、订单、运输、库存都会用到商品信息(Product),但是他们所关心领域知识不一样。如果没有边界的,Product类就要包含与之相关的领域知识,使得Product领域模型变得越发臃肿。对于这种情况,可以根据限界下文拆分出不同的Product类,比如在采购上下文Product只会关心采购相关的领域知识。限界上下文告诉我们,同一个概念,不必总是对应于一个单一模型,也可以对应于多个模型。
1.3 上下文映射定义:上下文映射是指将不同的限界上下文(Bounded Context)之间的概念和模型进行映射(Context-Mapping)和协调的过程。限界上下文之间映射,表达了不同领域之间如何进行交互和协同,领域之间的交互是一个复杂的问题,很多时候不仅仅和技术相关,更重要还会涉及到组织结构或者责任分配。比如:XXX领域为啥不按照我的要求提供数据,这个时候到底是谁问题呢,当我为多个领域提供服务时候,他们都对我提出很多要求。 案例:用户下单为例, 需检查商品库存量、提交订单时候需要锁定库存、和使用优惠。由此就产生了订单上下文和库存上下文、营销上文的协作必然带来领域知识传递;如果上游的提供的服务总在变化,订单开发者就需要采用一定的设计手段来避免服务变化带来的干扰。 DDD上下文映射主要有解决方案:共享内核、防腐层、开放主机服务、客户-供应方、遵奉者等。
1.4 核心域、支撑域、通用域领域在不断细分&拆解不同子域,这些子域可以根据自身的重要性和功能属性,可以分为三大类,即:核心域、通用域、和支撑域,如下图所示,其中核心域是重心,支撑域和通用域为了支撑核心域的。值得说明是核心域只是一类统称,核心域可以包含多个功能子域。 核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。当然我们不能说支撑子域和通用子域是不重要的,它们也是重要的,只是我们对它们的要求并不像核心域那么高。
2 战术设计战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:实体、值对象、聚合根、领域服务、领域事件、应用服务和资源库等代码逻辑的设计和实现。
2.1 实体定义:在DDD中实体是指具有唯一标识符和生命周期的对象或事物。实体通常具有以下特征:
身份标识;可以用唯一的标识符来区分不同的实体。生命周期;可能存在创建、修改、删除等状态转换。属性和行为;属性是实体静态特征和行为是实体的动态特征。 其中这里唯一标识和可变性将实体和值对象从本质上区分开来。(PS:实体为啥需要唯一标识而值对象不需要?) 身份标识:有些实体身份标识规定了一些组合规则,如:Citizen实体的身份证号,遵循了一些业务规则,这样的身份蕴含了领域知识,体现了领域概念。 public interface Identity <T>{ T nextValue(); } public class UserSignIdGenerator implements Identity<Long> { @Autowired @Qualifier("userSignUpSequence") private GroupSequence userSignUpSequence; @Override public Long nextValue() { return userSignUpSequence.nextValue(); } public long nextValue(long userId) { //这里128 是分128张表,分表建按照userId return nextValue() *128 +userId % 128; } }生命周期:比如玩法活动实体->活动开始->活动进行中->活动结束 性和行为:实体的属性用来说明主体的静态特征,根据粒度可以将属性分为原子属性和组合属性(组合属性可以是值对象或者实体)。划分标准是该属性是否存在约束规则、组合因子或者属于自己的领域行为。
public class UserVipAccountDO /** * 会员ID */ private Long vipId; /** * 手机号 */ private PhoneNumber phoneNumber; } // 比如一个手机号,有业务规则约束,比如11位等,这样的话,设计成PhoneNumber类更合适 // 这里等级其实是组合因子,比如等级名称、描述、升级规则等 public class PhoneNumber { private final String number; public String getNumber() { return number; } public PhoneNumber(String number) { if (number == null) { throw new IllegalArgumentException("number不能为空"); } else if (isValid(number)) { throw new IllegalArgumentException("number格式错误"); } this.number = number; } public static boolean isValid(String number) { String pattern = "^0?[1-9]{2,3}-?\\d{8}$"; return number.matches(pattern); } }领域行为:实体拥有领域行为,可以更好的说明其作为主体的动态特征;领域行为的方法名应该从业务角度表达领域逻辑。
public class PlayActivityDO extends BaseDO { /** * 主键-玩法活动id */ private Long playActivityId; // ..... 修改的只是对象内存状态和持久化无关 public void delayActivityTime(Date endTime) { // do } } 2.2 值对象定义:值对象 (Value Object) 是指在软件系统中表示不可变信息的对象。它不具有业务逻辑,也不会改变状态。值对象具有以下特点:
度量或描述:只是用于度量或者描述领域中某件东西的一个概念。比如:一个人拥有年龄、地址等。不可变性:值对象的值在创建后就不能被修改,任何修改操作都会返回一个新的值对象;值语义:值对象的相等性是基于其属性值的相等性,而非引用的相等性。可替换性:值对象可以被其他具有相同属性值的值对象替代,不会对系统的行为产生任何改变。 值对象也会有一些自给自足的领域行为。如自我验证,自我组合,自我运算。 public class Money { private final BigDecimal amount; private final Currency currency; public BigDecimal getAmount() { return amount; } public Currency getCurrency() { return currency; } public Money(BigDecimal amount, Currency currency) { this.amount = amount; this.currency = currency; } //自我组合 public Money add(Money toAdd) { if(!currency.equals(toAdd.currency)) { throw new IllegalArgumentException("different currency can not add!"); } return new Money(amount.add(toAdd.amount), currency); } .... } 2.3 聚合定义:聚合是包含了实体和值对象的一个边界,选择一个实体作为这个聚合的根,并允许外部对象仅能持有聚合根的引用。常见误区:
聚合是边界,不是对象,比如玩法活动实体为玩法的聚合,不存在一个玩法活动聚合对象。将OO关联关系(组合、聚合)和DDD聚合混为一谈 如下图所UserVIPInfo对象和BenefitRecord对象之间存在1 :n的的聚合关系,但是在DDD设计会把他分配到两个聚合中去。 聚合的设计原则:完整性、独立性、不变量和一致性。 完整性:聚合作为一个受到边界控制的领域共同体,对外由聚合根体现为一个统一的概念。如:玩法活动实体。 独立性:设计小的聚合,应该优保证独立性,再考虑完整性。完整性除了通过聚合来保证,也可以通过聚合之间的关系来保证。 不变量:在数据变化时必须保持一致性规则,涉及聚合成员之间的内部关系,是约束聚合内对象之间的关系。不变量代表了领域逻辑中的业务规则或者验证条件,有时候可以将不变量理解为不变条件或者固定规则。 如:要求一个问题必须有四个答案,这种就是业务不变量的约束规则。反例: 一致性:一致性约束可以理解为事务的一致性,即在事务开始前和事务结束后,数据库的完整性约束没有被破坏。一个聚合必须满足事务的一致性,反之则不尽然. 2.4 领域服务定义:领域中服务表示一个无状态的操作,它用于实现特定于某个领域任务。当某个操作不适合放到聚合和值对象上时(比如跨聚合的协作),这个时候便可以考虑领域服务了(领域服务是领域建模的最后选择)。领域服务并不映射真实世界的领域概念(名词),而单纯地体现一种领域行为(动词),要求领域服务名称必须包含动词。避免出现:CouponDomainService 这种,建议使用:CouponExchangeDomainService。
2.5 领域事件定义:领域事件是领域模型的组成部分,表示领域中所发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。如:当什么发生时候…请通知我,当什么完成时候…请通知我。
@Getter public abstract class DomainEvent { //领域事件发生时间 private final long timeStamp; //领域事件Id,本身没有任何业务含义 private final String eventId; public DomainEvent() { this.timeStamp = System.currentTimeMillis(); this.eventId = UUID.randomUUID().toString(); } } @Getter public class UserSignedEvent extends DomainEvent { /** * 报名记录Id */ private final Long signRecordId; /** * 这里活动Id是玩法活动Id */ private final Long activityId; /** * 报名用户userId,淘系登录 */ private final Long userId; public UserSignedEvent(Long signRecordId, Long activityId, Long userId) { super(); this.signRecordId = signRecordId; this.activityId = activityId; this.userId = userId; } }参考文献 【1】 time.geekbang.org/column/article/149943 【2】 martinfowler /articles/microservices.html 【3】张逸. 解构领域驱动设计[M]. 北京:人民邮电出版社,2021.09 【4】Vaughn Vernon.实现领域驱动设计[M]. 北京:电子工业出版社出社,2014
你需要知道DDD基本知识由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“你需要知道DDD基本知识”