GitChat课程《领域驱动设计--战略篇》笔记,课程作者张逸
一.理解限界上下文
1.限界上下文的定义
- 限界上下文:Bounded Context
1)上下文(Context)表现业务流程的场景片段
2)整个业务流程由诸多具有时序的活动组成,随着流程的进行,不同的活动有不同的角色参与,并导致上下文切换
3)上下文(Context)其实是动态的业务流程被边界(Bounded)静态切分的产物 - 以咨询师从成都到深圳为客户提供服务的场景为例理解限界上下文
1)相同的人物在不同的上下文参与不同的活动,履行不同的职责
2)整个业务流程由诸多分散且目标不同的活动(Actions)组成,这些活动在同一个上下文中为同一目标提供服务
- 理解限界上下文的关键点
1)知识:不同的限界上下文需要不同的领域知识,这实际上就是业务相关性
2)角色:参与到这个上下文的对象扮演何种角色,各种角色如何协作
3)边界 - 根据业务相关性、耦合强弱程度、分离的关注点对业务的活动进行归类,找到不同类别之间存在的边界,这就是限界上下文的含义
2.限界上下文的价值
- 对不同边界的控制力
1)领域逻辑层面:限界上下文确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度
2)团队合作层面:限界上下文确定了开发团队的工作边界,建立了团队之间的合作模式,避免团队之间的沟通混乱,从而降低系统开发的管理复杂度
3)技术实现层面:限界上下文确定了系统架构的应用边界,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低系统的技术复杂度 - 限界上下文是满足下述四个特点的自治单元
1)最小完备:自治单元履行的职责是完整的,同时避免添加不必要的职责
2)自我履行:由自治单元自身决定要做什么,对于不属于自身的行为应转交给其他上下文
3)稳定空间:减少外界变化对限界上下文内部的影响
4)独立进化:减少限界上下文变化对外界的影响
3.限界上下文的控制力
- 限界上下文分离了业务边界
如在电商系统中,产品实体Product在不同的限界上下文有不同的含义,关注的属性与行为也不尽相同
1)在采购上下文,需要关注产品的进价、最小起订量与供货周期
2)在市场上下文,则关心产品的品质、售价,以及用于促销的精美⽚片和销售类型
3)在仓储上下文,仓库⼯作⼈员更关心产品的位置,重量与体积,是否易碎品以及订购产品的数量
4)在推荐上下文,系统关注的是产品的类别、销量、收藏数、正面评价数、负面评价数。
理想的设计方案是让每一个限界上下文拥有自己的领域模型Product
- 限界上下文明确了工作边界
1)2PTs(Two-Pizza Teams)规则:让团队保持在两个披萨能让成员吃饱的规模(7-10人)
2)康威定律(Conway's Law):任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的沟通结构保持一致
3)DDD按照软件的特性(Feature)而非组件(Component)来组织软件开发团队
4)组件团队:数据库+前端+后端,更容易发挥每个人的技能特长,但导致团队成员缺乏对业务的了解,任何修改都要横跨多个组件团队,沟通成本较高
5)特性团队:端对端的开发垂直细分领域的跨职能团队,将需求分析、架构设计、开发测试等多个角色糅合在一起,专注于领域逻辑
- 限界上下文封装了应用边界
划分限界上下文不只从业务边界确立,还要考虑控制技术复杂度,从而保证系统质量,例如
1)高并发:外卖系统的订单业务与门店、支付等领域存在业务相关性,但外卖订单在高峰期存在高并发压力,将订单业务作为一个单独的限界上下文,可以从物理架构上保证独立性,在资源分配上做到高优先级地扩展
2)功能重用:⼀个⾯向企业雇员的国际报税系统,报税业务、旅游业务与 Visa 业务都需要账户功能的⽀撑。系统对⽤户的注册与登录有较为复杂的业务处理流程。从功能重用的角度考虑,应该将账户管理作为一个单独的限界上下文,以满足核心领域对账户管理的功能重用
3)实时性:电商系统中,价格是商品概念的一个重要属性,仅仅从业务角度考虑,在进行领域建模时,价格仅仅是一个普通的领域值对象,但电商系统的商品数量达到数十亿种,每天获取商品信息的调用量在峰值达到数亿乃至数百亿次时,价格就从业务问题变成了技术问题。为了保证高并发下价格的实时性,可以将价格领域作为一个独立的限界上下文,形成自己与众不同的架构方案,以满足高并发和实时性的要求
4)第三方服务集成:电商系统需要支持多种常见的支付渠道,如微信、支付宝、银联。可以将支付服务集成划分为一个单独的限界上下文,一方面为支付服务额客户端提供完全统一的支付接口,保证接口调用上的便利性与一致性,另一方面解除第三方支付服务与电商系统内部模块之间的耦合,避免“供应商锁定”
5)遗留系统:将遗留系统划分为一个限界上下文,由于新增需求与原有系统在业务上存在交叉功能,因而可能失去部分代码的重用机会,但可以避免在新功能开发过程中陷入遗留系统庞大代码库的泥沼
二.识别限界上下文
- 从业务边界、工作边界、应用边界三个层次识别
1.从业务边界识别限界上下文
- 在梳理主要业务流程后,抽象出不同的业务场景,最后总结出业务活动的描述
- 从语义相关性分析业务活动的描述,如果是相同的语义,可以作为归类的特征
- 从功能角度去分析业务活动,如果存在关联和依赖,可以作为归类的特征
2.从工作边界识别限界上下文
- 确定限界上下文合理的工作粒度
- 团队之间是“渗透性边界”,不能太封闭(拒绝外部输入),也不能太开放(失去内聚力)
3.从应用边界识别限界上下文
- 质量属性:利用限界上下文将可能改变系统架构的风险控制在一个极小范围内
- 重用和变化
1)运用重用原则分离的限界上下文对应于支撑子领域,作为上游上下文为其他上下文提供业务支撑
2)一个限界上下文不应该存在两个引起它变化的原因 - 遗留系统:对遗留系统中需要替换的组件进行抽象,从而将消费者与遗留系统中的组件实现进行解耦,最后提供一个新的组件实现,在保留抽象层接口不变的情况下替换遗留系统的旧组件,完成技术栈迁移
三.上下文映射
1.上下文映射概述
- 领域驱动设计通过上下文映射(Context Map)来讨论限界上下文之间的协作问题
- 上下文映射是一种设计手段,包括
1)共享内核(Shared Kernel)
2)防腐层(Anticorruption Layer)
3)开发主机服务(Open Host Service)等模式 - 两个限界上下文之间的关系方向由术语上游(UpStream)和下游(DownStream)描述
1)限界上下文影响作用力的方向与程序员惯常理解的依赖方向相反,上游影响下游,意味下游依赖于上游
2)下游上下文中的用例才是核心领域,而上游限界上下文是下游限界上下文的功能支撑
3)例如在下订单场景中,订单上下文调用支付上下文,支付上下文是提供功能支撑的上游上下文,订单上下文是下游上下文,下订单是核心领域
- 上下文映射模式分为
1)团队协作模式:对应于团队(上下文)合作的工作边界
2)通信集成模式:从应用边界的角度分析了限界上下文之间该如何通信才能提升设计质量
2.上下文映射模式1:团队协作
- 团队协作应遵循“各司其职,权责分明”的模式,在满足合理分配职责的前提下,谨慎地确保每个限界上下文的粒度
- DDD根据团队协作的方式与紧密程度,定义了合作、共享内核、客户方-供应方开发、遵奉者、分离方式五种团队协作模式
- 合作关系(Partnership)
表示两个限界上下文的团队存在要么一起成功要么一起失败的强耦合关系,甚至是糟糕的双向依赖,对于这种槽糕的何种关系,通常有三种解决方法:
1)既然两个限界上下文存在如此紧密的合作关系,说明与其拆分,不如让它们合并在一起
2)将产生特性依赖的职责分配到正确的位置,尽力减少一个方向的多于依赖
3)识别产生双向依赖或循环依赖的原因,然后将它从各个限界上下文中剥离出来,成为单独的限界上下文,即所谓的“共享内核(Shared Kernel)” - 共享内核(Shared Kernel)
共享内核用于避免重复。这种重用以牺牲限界上下文自由更改的能力为代价 - 客户方-供应方开发(Customer-Supplier Development)
团队合作中最常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系。这种合作需要两个团队协商以下问题:
1)下游团队对上游团队提出的领域需求
2)上游团队提供的服务采用什么样的协议与调用方式
3)下游团队针对上游服务的测试策略
4)上游团队给下游团队承诺的交付日期
5)当上游服务的协议或调用方式发生变更时,该如何控制变更 - 遵奉者(Conformist)
客户方-供应方开发模式,是上游团队满足下游团队提出的领域需求;而遵奉者模式,是由上游团队来决定是响应还是拒绝下游团队提出的请求。遵奉者模式意味着
1)可以直接重用上游上下文的模型(好的)
2)减少了两个限界上下文之间模型的转换成本(好的)
3)使得下游限界上下文对上游产生了模型上的强依赖(坏的) - 分离方式(Separate Ways)
分离方式的合作模式指两个限界上下文没有任何关系
例如在电商网站中,支付上下文与商品上下文就是分离方式,而货币上下文其实是支付上下文
3.上下文映射模式2:通信集成
- 利用防腐层和开放主机服务降低限界上下文之间的耦合关系
- 防腐层(Anticorruption Layer)
1)在架构层面,通过引入防腐层有效隔离限界上下文之间的耦合
2)防腐层同时还可以扮演适配器、调停者、外观等角色
3)防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化
- 开放主机服务(Open Host Service)
1)开放主机服务即定义公开服务的协议,包括通信的方式、传递消息的格式(协议),保证开放的服务不会轻易做出变化
2)防腐层是下游限界上下文对抗上游变化的利器,开放主机服务是上游服务用来吸引更多下游使用者的诱饵
3)上游限界上下文往往会被多个下游限界上下文消费,如果通过防腐层的形式需要为每个下游提供一个相似的防腐层,冗余度高 - 发布/订阅事件
1)即使确定了发布语言规范的OHS,仍然会导致两个上下文之间存在耦合关系,下游限界上下文必须知道上游服务的ABC(Address、Binding与Contract),对于不同的分布式实现,还需要在下游定义类似服务桩的客户端
2)发布/订阅事件的方式在解耦合方面走得更远。一个限界上下文作为事件的发布方,另外的多个限界上下文作为事件的订阅方,二者的协作通过经由消息中间件进行传递的事件消息来完成。在确定消息中间件后,发布方与订阅方唯一存在的耦合点就是事件持有的数据
3)实例:从买家搜索商品并将商品加入购物车开始,到下订单、支付、配送完成订单结束,整个过程通过发布/订阅事件方式由多个限界上下文一起协作完成
四.辨别限界上下文的协作关系
1.限界上下文的通信边界对协作的影响
- 限界上线的通信边界分为进程内边界与进程间边界,通信边界直接影响上下文映射模式的选择
- 进程间边界需要考虑跨进程访问的成本,如序列化和反序列化、网络开销等。由于跨进程调用的限制,彼此之间的访问协议也不尽相同,同时还需要控制上游限界上下文可能引入的变化,一个典型的协作方式是同时引入开放主机服务(OHS)与防腐层(ACL)
1)限界上下文A对外通过控制器(Controller)为用户界面层暴露REST服务,而在内部则调用应用层的应用服务(Application Service),然后再调用领域层的领域模型
2)如果限界上下文A需要访问限界上下文B的服务,则通过领域层的接口(Interface)调用基础设施层的客户端(Client)完成,这个客户端即限界上下文A的防腐层
3)限界上下文B访问限界上下文C的方式完全一致,限界上下文C则通过资源库(Repository)接口经由持久化(Persistence)组件访问数据库
2.协作即依赖
- 两个限界上下文存在协作,意味着彼此存在依赖关系,一方需要知道另一方的知识,包括
1)领域行为:导致行为之间耦合的原因是什么?如果是上下游关系,要确定下游是否是上游服务的真正调用者
2)领域模型:需要重用别人的领域模型,还是自己重新定义一个模型
3)数据:是否需要限界上下文对应的数据库提供支撑业务行为的操作数据
这三种知识都将产生依赖
3.领域行为及其产生的依赖
- 领域行为
领域行为在设计层面就是每个领域对象的职责,职责可以由实体(Entity)、值对象(Value Object)、领域服务(Domain Service)、资源库(Repository)或者工厂(Factory)对象承担。包括三种履行职责的方式
1)亲自完成所有工作
2)请求其他对象帮忙完成部分工作(和其他对象协作)
3)将整个服务请求委托给另外的帮助对象 - 领域行为产生的依赖
1)当领域对象履行职责的方式为上述的后两种时,必然牵涉到对象之间的协作
2)每个领域对象应该只承担自己擅长处理的部分,而将自己不擅长的职责转移到别的对象
3)实例:电商系统业务场景客户已经选择好要购买的商品,并通过购物车提交订单,那么提交订单的职责应该由客户上下文还是订单上下文履行?
领域行为产生的依赖可以通过抽象接口来解耦 - 实例:下订单场景的限界上下文架构
1)领域层处于限界上下文的核心
2)应用层包裹整个领域层,通过RESTful服务与作为调用者的前端通信
3)RESTful服务等同于上下文映射中的开放主机服务(OHS),或MVC模式的控制器(Controller),属于基础设施层的组件
3.领域模型产生的依赖
- 以查询客户订单信息为例,可以引入资源库对象来履行查询职责。查询订单时,将SaleOrder作为聚合根,对应的SaleOrderRepository作为资源库放到订单上下文。在分层架构中,资源库对象可能会被封装到应用服务中,也可能直接暴露给作为适配器的REST服务,例如
@Path("/saleorder-context/saleorders/{customerId}")
public class SaleOrderController {
@Autowired
private SaleOrderRepository repository;
public List<SaleOrder> allSaleOrdersBy(CustomerId customerId) {
return repository.allSaleOrdersBy(customerId);
}
}
REST服务的调用者并非客户上下文,而是前端或客户端,从而接触客户与订单的包含关系
- 以订单上下文在查询订单时需要获得订单对应的商品信息为例,应该采用遵奉者模式,即在订单上下文重用商品上下文的领域模型?还是重新在订单上下文定义属于自己的与商品有关的领域模型?
1)选择前者,即重用商品领域对象时,可以提高代码重用,在今后的修改中避免散弹式的修改,但是由于两个限界上下文对商品的需求不同,重用的商品模型需要同时应对两种不同的需求,从而主键成为一个低内聚的对象
2)选择后者,即分别为商品上下文和订单上下文建立商品领域对象,虽然会带来代码的重复,但分离的两个模型可以独自应对不同的需求变化,即"独立演化"。 - 因此,常常在两个不同的限界上下文为相同或相似的领域概念分别建立独立的领域模型,如下图所示
4.数据产生的依赖
- DDD中,通过领域模型的资源库访问数据库,与数据库交互的对象是领域模型对象(实体和值对象),即使有依赖,也是领域行为与领域模型导致的
- 有时候出于性能或其他原因考虑,一个限界上下文访问属于另一个限界上下文边界的数据时,会跳过领域模型而直接通过SQL或存储过程的方式对多张表执行关联查询。这种访问跨限界上下文数据表的方式确实是最简单最高效的实现方式,但需要限界上下文之间数据库共享
- 这种数据依赖值得警惕,SQL乃至存储过程形成的数据表关联难以解耦。一旦系统架构需要从单体架构(或数据库共享架构)演进到微服务架构,最大的障碍不是代码层面而是数据库层面的依赖,即大量复杂的SQL与存储过程
- SQL与存储过程的问题在于
1)无法为SQL和存储过程编写单元测试
2)SQL与存储过程的可读性通常较差,难以重用
3)SQL与存储过程的优化策略限制太大