领域驱动设计
详解DDD——用菜鸟的视角从零开始
DDD的基础
DDD出现的背景
我知道背景很没用很枯燥,但是对于我而言,如果不理解DDD的背景,我就无法知晓为什么要使用DDD,我现在写这段话时依旧觉得分层架构已经足够,DDD思想的整洁架构、菱形架构、六边形架构等都没有必要。因此只有了解背景,我们才能更好的理解:为什么要使用DDD。
软件工程体系中,我们最先学到的一定是瀑布模型,将软件工程的全生命周期分为有序的几个部分。对于时间充裕且变化小的软件来说,瀑布模型无疑是非常有效的,因为规范化的流程能让我们更加有序地开发出可用的软件。
但我也说了,是对于“时间充裕且变化小的软件”,当今互连网飞速发展,我们的需求可能在不断变化,开发的软件也越来越复杂。编码与设计的分离导致了程序员无法实现产品经理天马行空的想象,产品经理也在不知晓编码细节的情况下,会给出不完善的文档。而不完善的部分一旦很多,程序员不会去向产品经理寻求完善,而是为了赶工期,发挥自己的想象填补。这导致实际交付的软件与客户的需求大相径庭。
那么,程序员该如何将业务知识的学习和思考结合起来?如何通过有逻辑性的思考来提升自己的业务知识水平,从而编写出更专业的业务软件?
DDD思想和方法论的诞生,可以说初步解决了程序员的这种困惑。DDD建模思想不同于以往的面向对象分析设计思想那样,建模和代码之间还是存在落差,无法平滑衔接。它将分析和设计完美结合起来,通过引入上下文的特殊性,将项目的真正业务背景和集成复杂性引入设计建模阶段,虽然增加了设计的复杂性,但也提高了设计的实用性。不过,可能因为DDD引入了太多行话,导致其本身很难被传授。
DDD的发展
DDD还在发展之中,过去十多年中主要经历了三个阶段:首先是Eric Evans的理论原则创建和普及阶段;然后是引入领域事件、事件溯源阶段;最后是微服务架构的提出阶段。由于DDD提出的有界上下文已经将业务的边界划分清楚,所以微服务的实现就顺理成章了。当然,微服务架构的普及和发展也迅速促进了DDD的普及和发展。
同时,在人们不断丰富DDD的实现技术以后,突然回首才发现,DDD中的战略模式需要更多的关注,因此,事件风暴等有关组织管理等方面的新事物开始出现。通过事件风暴会议发现领域中的事件,对领域的上下文进行切分,发现其中的聚合,这套方法变得越来越流行。之所以会这样,是因为大家发现寻找领域或上下文边界才是DDD中最难,也是最需要创造力的地方。边界或有界上下文是DDD专门用于解决复杂性的有力武器,是DDD的核心内容。
那么,该如何识别边界和上下文呢?事件风暴(Event Storming)应运而生。事件风暴是指将产品经理和程序员聚在一起,进行头脑风暴,程序员能够更好地理解业务,产品经理也能够减少自己的逻辑矛盾之处。在事件风暴过程中,大家的认识逐渐一致,复杂性也慢慢被肢解成不同模块,上下文与边界也逐渐明确。
DDD使传统OO分析和设计不再割裂。传统OO分析和设计是分开的,先进行分析,再根据分析的结果进行设计,期间会出现分析阶段过于理想化,使得设计不得不对分析进行改变,最后交付的软件与分析相去甚远。DDD使分析和设计相互促进、相互优化、相互改正,分析的结果必须经过设计细节验证。传统的OO分析使用的是主语名词法,也就是识别对象,这种分析方法其实受到了ER数据模型的影响。问题在于,如果无法发现名词,人们就只能靠想象。事件风暴倡导从动词入手,识别领域中的事件,对这些事件进行分类,就得到了有界上下文和聚合。使用了分而治之思想。
当然,DDD也可以通过UML顺序图使用动词分析法,无序拘泥于形式,关键在于从动词入手,发现有界上下文和聚合。
DDD相关的概念
问题空间
问题空间是解决问题的目标所在,有了问题空间,才能提出解决方案
领域
定义:
- 领域就是软件要解决的业务问题的整体范围,是业务活动的全景图。
特点:
- 领域可以进一步拆分为子领域,每个子领域聚焦于一块相对独立的业务能力。
例子:
- 电商领域:商品管理、订单处理、支付结算、库存管理、物流配送、售后服务等。
- 在这个领域中,“订单处理”就是一个子领域,它包含订单创建、支付、发货、取消等业务规则。
界限上下文
定义:
- 限界上下文是领域模型的语义边界,在这个边界内,通用语言中的术语有唯一且明确的含义。
特点:
- 一个子领域可能包含一个或多个限界上下文
例子:
- 订单管理上下文:
- “订单”是主语,包含订单项、收货地址、支付状态等,负责订单生命周期(创建、支付、发货、取消)。
- 库存管理上下文:
- “库存”是主语,负责商品库存的增减、预警等。
- 这里的“订单”只是一个外部触发库存变动的来源,不包含订单的详细业务规则。
- 支付上下文:
- “支付交易”是主语,负责与第三方支付平台交互,处理支付成功/失败、退款等。
- 这里的“订单”只是支付请求的一个参数。
实体
定义:
- 有唯一标识(ID),生命周期内状态可变的业务对象。
特点:
- 通过 ID 区分,即使属性完全相同,只要 ID 不同就是不同实体。
- 关注的是身份和行为,而不仅是数据。
值对象
定义:
- 没有唯一标识,仅由属性值定义的对象,通常不可变。
特点:
- 相等性由属性值决定。
- 用于描述事物特征,表达业务概念。
- 不可变(修改 = 创建新对象)。
例如:
- 地址(Address):由省、市、街道等组成。
- 金额(Money):由币种 + 数值组成。
聚合与聚合根
定义:
- 聚合:一组相关实体和值对象的集合,作为数据修改和事务一致性的边界。
- 聚合根:聚合的入口,负责维护聚合内部不变量(业务规则)。
特点:
- 外部只能通过聚合根访问内部成员。
- 聚合内的修改必须保持业务一致性。
例如:
- 订单聚合(Order Aggregate):
- 聚合根:订单(Order)
- 内部实体:订单项(OrderItem)
- 值对象:收货地址(Address)、金额(Money)
工厂
定义:
- 封装复杂对象或聚合的创建逻辑的组件。
作用:
- 避免在聚合根构造函数中塞入过多初始化细节。
- 统一创建过程,确保对象创建时满足业务规则。
例如:
OrderFactory.createOrder(customerId, items)
:内部会创建订单、订单项、计算总价等。
仓储
定义:
- 面向领域层的持久化抽象,负责聚合的加载与保存。
特点:
- 隐藏数据访问细节(数据库、缓存、API)。
- 以聚合为单位进行存取。
例子:
OrderRepository.findById(orderId)
OrderRepository.save(order)
领域事件
定义:
- 领域内发生的、对业务有意义的事实,通常用过去式命名。
特点:
- 反映业务状态变化。
- 可用于解耦上下文(事件驱动架构)。
例子:
- OrderPaid(订单已支付)
- ProductOutOfStock(商品已缺货)
几种模型
上述DDD相关概念读者在第一次听说后可能会产生困惑,因为我们并没有实际在开发中使用DDD。现在给大家讲解几种模型来更好的理解。
失血模型
失血模型中,数据只有属性和getter/setter,没有其他函数。
贫血模型
贫血模型很好理解。在我们的分层架构中,通常会定义实体类,这些实体类通常除了getter/setter和一些基础函数就没有其他方法了。这种属性与具体业务逻辑分离的数据模型就是贫血模型。
充血模型
与贫血模型相反,充血模型中对象既有状态(属性)又有行为(方法),行为直接作用于对象自身的状态。业务规则、计算、状态变更等逻辑放在领域对象内部,而不是全部集中在 Service 层。满足面向对象的封装思想:数据和操作数据的逻辑放在一起。
可以看出,充血模型非常适合DDD的思想,可以更好地实现有界上下文这一概念。