第一章DDD对我而言
还可以指引构建正确软件模型的方向。
领域驱动对团队人的要求较高:
- 具备深厚的业务能力(领域专家)
- 具备业务抽象能力;
- 具备技术抽象能力
DDD 领域驱动设计 可以实现目标
- 如果你希望打磨软件匠艺并提高项目的成功率;
- 如果你迫切期望创造软件来帮助企业把业务竞争力提升到新高度;
- 如果你期望实现出来的软件既能正确地对业务需求建模又可以采用最新建的软件架构进行扩张;
设计
设计是不可或缺的,除了优秀设计就是糟糕设计,根本不存在不做设计.
有效设计(Effective Design)
可以满足商业组织希望借助软件超越竞争者的诉求,它可以驱动企业去思考哪些核心业务必须成为其竞争力,
战略设计
强调业务战略上的重点,如何按重要性分配工作,以及如何进行最佳
- 运用限界上下文(Bounded Context)的战略设计模式来分离领域模型;
- 在明确的限界上下文中发展一套领域模型的通用语言;
- 通过子域处理遗留系统中无边界的复杂性,以及如何改进新项目上的成果;
- 通过上下文映射来集成多个限界上下文;
战术设计
战略设计强调的框架,战术设计强调的是细节;
聚合模式(Aggregate):将若干实体和值对象以恰当的大小聚集在一起;领域事件(Domain Events):既可以让你明确的建立模型,也可以把模型内部发生的事情分享给需要知道这一切的系统
第二章 运用限界上下文与通用语言进行战略设计
DDD主要关注的是如何在明确的限界上下文中创建通用语言的模型;
什么是限界上下文?
- 是语义和语境上的边界;
- 边界内每个代表软件模型的组件都有特定的含义并处理特定的事务;
- 限界内的组件有特定的上下文语境和语义;
- 限界上下文可以理解为问题空间的一部分;
- 软件模型逐渐清晰时,限界上下文将会被迅速转到解决方案空间;
- 模型在限界上下文中实现;
- 当限界上下文被当作组织的关键战略举措进行开发时,即被称为核心域。
- 强调内部的严谨性;
问题空间
是在给定项目的约束条件下进行高级战略分析和设计各个步骤的地方。简单直白些,就是为了完成目标的问题与风险;
解决方案空间
是真正实施解决方案的地方,这些解决方案在问题空间讨论中被识别为核心域。
通用语言
在限界上下文中发展了一种语言用于表达其边界内的软件模型,这一语言由在该限界上下文中开发软件模型的每个团队所使用。
- 是软件模型团队日常交流时使用的语言
- 软件模型的源代码就是这种语言的书面表达方式。
必须:严谨、精确、紧凑
核心域(core Domain)
当限界上下文被当作组织的关键战略举措进行开发时,被称为核心域。
- 最重要的软件模型(取得成功的手段)
- 使组织在与其他组织竞争中脱颖而出
- DDD的首要价值主张
- 也是资源需要重点投入的地方 我们必须将核心域限制在最基本的模型元素范围内,否则交付的就不是最优价值的;
限界上下文、团队和源代码仓库
- 一个团队应该在一个限界上下文中工作;
- 每个限界上下文应该拥有一个独立的源代码仓库;
- 一个团队可能工作在多个限界上下文中;
- 多个团队不能在同一个限界上下文上;
- 必须通过接口来调用限界上线文(微服务本身也是这种思想);
大泥球(Big Ball of Mud)
- 系统由多个没有明确边界并纠缠在一起的模型组成
- 各种毫不相干的概念充斥在众多的模型中;
- 各种自相矛盾的元素相互关联;
- 多个团队在其中工作;
领域专家和业务驱动
- 有一个好的业务专家,能理清各业务边界;
- DDD强调不同的概念类型分离到不同的限界上下文中,以此来拥抱变化;
战略设计是必要的根基
限界上下文和通用语言,是战略设计部分基本工具;限界上下文会明确什么是核心;
测试收益:测试会聚焦于一个模型中,这样测试的数量会更少,执行更快。
只有经过“仅限核心”的严格过滤之后保留下来的概念,才能成为拥有限界上下文的团队的通用语言一部分。限界上下文的边界强调其内部的严谨性;
如何确定核心?
领域专家(Domain Expert)和软件开发人员通力合作来确定核心;
领域专家:主要专注于业务,对业务进行规划、抽象;领域专家的心智模型将会成为团队通用语言的坚实基础;
开发人员:将精力花费在编程语言和技术研究中;
DDD:专注业务复杂性而非技术复杂性;有利于业务模型高度复杂的项目开发;
通用语言:是通过协作反馈循环而发展出来的,从中可以促成团队形成共同的心智模型(产品与技术的目标对齐);
发展通用语言
我们应当使用一组具体场景来表达核心域,而不要将核心域局限在名词上。使用DDD赋予的能力:在于可以真正地通过对话了解领域模型如何设计。
推导应用场景
使用被实例化需求的技术(行为驱动开发BDD) 推导出来里面包含的业务逻辑节点,使用假如/当/那么(Given/When/Then)的方法来推导 在业务逻辑推导出来后,单元测试的框架也就出来了;
持续学习改进通用语言
架构
六边形架构:
端口和适配器的“应用-端口-适配器”是“由内向外”的三层, 它将核心业务逻辑(应用层和领域层)和外层的API接口(端口层)以及外部各种具体实现的依赖(适配器层、如各种前端界面、数据库、第三服务、消息机制等)解耦开 通过依赖注入等手段,让架构更具灵活性和可扩展性的同时,也让团队把更多的精力聚焦在核心的应用层上;
三层应用架构:数据-应用-展现
领域模型本身和技术无关;
除了六边形架构,其他可用架构
- 事件驱动架构:事件溯源
- 命令和查询职责分离
- 响应式架构和Actor模型
- 具象状态传输(REST)
- 面向服务的架构(SOA)
- 微服务架构,其本质等同于DDD中的限界上线文
第3章 运用子域进行战略设计
什么是子域?
- 子域是整个业务领域的一部分
- 子域代表的是一个单一的,有逻辑的领域模型
- 一个明确的专业领域
子域类型
- 核心域(Core Domain) : 唯一的,定义明确的领域模型,要对它进行战略投资,并在一个明确的限界上下文中投入大量资源去精心打磨通用语言,必须把核心域打造成组织的核心竞争力;
- 支撑子域(Supporting Subdomain):建模场景提倡“定制开发”,会考虑外包的方式实现
- 通用子域(Generic Subdomain):可采购现成的
应对复杂性
- 子域可以作为讨论问题空间的工具
- 用网关屏蔽各子域的差异性
- 我们将逻辑模型当成一个子域对待,并且分开通用语言,可以避免系统形成大泥球
- 使用子域思考和讨论遗留系统,也可以避免形成大泥球
- 如果必须在同一个限界上下文中创建第二个模型,应该使用同一个完全独立的模块将该模型从核心域中分离出来;
第4章 运用上下文映射(Context Mapping)进行战略设计
当项目的核心域必须和其他限界上下文进行集成,这种集成关系在DDD中称为上下文映射(Context Mapping);
在DDD中,链接两个限界上下文之间的这条限定就代表了上下文映射。每个限界上下文中都有一种通用语言,通过上下文映射,将两个限界打通;
映射的种类
合作关系
- 每个团队各自负责一个限界上下文;
- 两个团队通过互相依赖的一套目标联合起来形成合作关系;比如金融云团队中,贷前、贷中、贷后,就是一种合作关系。
共享内核
- 两个/更多团队之间共享着一个小规模但却通用的模型;
- 团队必须就要共享的模型元素达成一致。如:万卡的core包,以及用户提供core服务;
共享内核:常见的方式就是将通用模块通过jar依赖的方式共享给所有上下文使用;
** 客户-供应商**
描述的是两个限界上下文之间和两个独立团队之间的一种关系。
供应商位于上游(U):供应商提供了客户必须的东西,同时也制定了获取的标准;
客户位于下游(D):依赖供应商提供的服务
如:支付、三方前置
跟随者
跟随者关系存在于上游团队和下游团队之间,上游团队没有任何动机满足下游团队的具体需求,也可以理解为发布-订阅模型。
比如:在万卡借贷的核心流程中,大数据/营销团队是万卡核心流程的跟随者。在核心业务流程中,生成的事件,不会考虑下游系统所需的字段,只是如果在过程中自己用到了,就带着,如果不用,不会为了下游的跟随者去获取对应的数据放到事件中。
万卡与万卡商城也是跟随者模型,万卡商城的客户依赖于万卡,流量也依赖于万卡。
防腐层
防腐层是最具防御性的上下文映射关系,下游团队在其通用语言(模型)和位于它上游的通用语言之间创建了一个翻译层。
常见的防腐层-API网关
金融云的前置就是一个典型的防腐层,将对接的机构的接口形式转成贷中、贷后需要的模型,屏蔽了差异性。
服务总线bus也是一个防腐层。
开放主机服务
开放式主机服务会定义一套协议或接口,让限界上下文可以被当作一组服务访问。
如:rest服务,万卡-金融云-风控都是根据开放主机服务交互。
特别是对外服务的openApi,每次升级都必须兼容或者保留原有的服务。
已发布语言 已经发布的接口定义的通用语言。
各行其道
没有标准,随时会变,不能作为强依赖。特别是舆情监控里,需要去爬取微博或者百度,或者淘宝,就是一个各行其道。
大泥球 如之前的万卡API,职责定义不够清晰,各种需求柔和。
以下操作可能会变为大泥球
- 越来越多的聚合因为不合理的关联和依赖而交叉污染;
- 对大泥球的一部分进行维护就会牵一发而动全身,解决问题就像“打地鼠”;
- 只有同时了解各个系统通用语言的人才能保持项目的稳定;
善用上下文关系
- 基于数据库的集成方式一定要避免(如果一定要,通过防腐层来隔离要去集成的和适配的模型)
三种可靠的集成类型
基于SOAP的RPC(dubbo也可以看成是一种)的集成
区别是:soap是一套标准,dubbo通过提供api的jar包来完成。
需要注意:
- 网络瘫痪;
- 网络延迟;
- 之间是紧耦合;
基于SOAP的RPC缺乏健壮性,网络或者服务出现问题,容易挂。dubbo的负载能解决部分问题。
基于restful http的集成
接口设计要注意:
- 对外屏蔽模型与逻辑;
基于消息机制的集成
- 使用消息机制是最健壮的集成方法之一;
- 可以消除阻塞性质耦合;
- 领域事件由限界上下文中的聚合(Aggregate)发布;
- 消费者不应该使用事件发布者定义的事件类型;
- 应保证至少一次投递;
- 接受者必须实现幂等接收;
上下文映射示例
增强事件与反向查询
增强事件:填充足够多的数据增强领域事件来满足所有的消费者的需求;
反向查下:领域事件中只推送业务主键,通过主键反查自己需要的信息;
- 事件发布时,只需要将自己持有的数据发布即可,可将事件结果进行状态包装,避免查库或远程调用后加工;(包装发布业务的稳定性)
- 在事件发布时,注意数据安全;
第5章 运用聚合进行战术设计
什么是实体?
一个实体模型就是一个独立的事务,每个实体都拥有一个唯一的标识符,可以将他的个体性所有其他类型相同或不同的实体区分开;
值对象
- 值对象是用来描述、量化或者测量一个实体。
- 一个值对象不是事物;
- 值对象,就是一个值(Value)
聚合
聚合是由一个或多个实体组成,其中一个实体被称为聚合根。
- 一个或多个实体;
- 值对象
- 聚合的根实体控制这所有聚集在其中的其他元素;
- 每个聚合都会形成保证事务一致性的边界;
事务
- 使用事务实现细节;
- 隔离对聚合的修改;
- 保证业务不变性;
- 在每一次操作中都保持一致;
- 事务边界由商业动机决定;
- 业务规则是事务的驱动力,最终决定在单次事务里,哪些对象必须是完整;
聚合的经验法则
聚合的四条基本规则:
- 在聚合边界内保护业务规则不变性
- 聚合要设计的小巧
- 只能通过标识符引用其他聚合
- 使用最终一致性更新其他聚合
在聚合边界内保护业务规则不变性
聚合的组成部分应该有业务最终决定;
聚合要设计的小巧
- 聚合的内存占用和事务包含范围应该相对较小
- 聚合的设计要满足单一职责
- 但也要防止过度设计导致项目的复杂度提升;
只能通过标识符引用其他聚合
- 标识符可以理解为业务的主键(这要根据实际场景考虑,防止数据重复获取)
- 保持聚合设计的小巧又高效
- 使用标识符使聚合可以使用任何类型的持久化机制轻松存储;
使用最终一致性更新其他聚合
- 通过领域事件完成最终一致性;
- 在完成最终一致性的同时,要处理消息的积压与丢失问题;
- 同时要考虑异常情况导致的中断;
建立聚合模型
贫血模型:模型好处了公有访问(Getter 和Setter)之外没有包含任何真正的业务行为;贫血模型在函数式编程时可以作为一种规范标准,因为函数式编程宣扬的是数据和行为的分离;
- 我们要把领域模型中的业务逻辑放到上层的应用服务中;
- 领域模型应该由持久化对象转化而来;
- 领域模型应该是当前业务逻辑所需数据的组合;
- 领域模型应该是数据与行为的一种聚合;
聚合设计的步骤:
- 为聚合根实体创建一个类(必须拥有全局唯一的标识符)
- 记录在查找聚合时必须用到的内在属性或者字段;
- 适当的屏蔽setter
- 添加聚合要实现的具体行为;
例如
`public class Auth{`
`//授信项,聚合的值对象`
`private String itemName;`
`//授信状态`
`private String authState;`
`// 创建时间`
`private Date createTime;`
`//customerId+ tenantId 就是一个全局唯一标识符`
`//用户的customerId (不可变的值对象)`
`private CustomerId customerId;`
`//租户id(不可变的值对象)`
`private TenantId tenantId;`
`//构造函数中不带状态`
`Auth(CustomerId customerId,TenantId tenantId,String itemName){`
`this.customerId = customerId;`
`this.tenantId = tenantId;`
`this.itemName = itemName;`
`}`
`// 这里只有行为`
`public void init(){`
`//初始化前的校验,需要将行为和数据入库操作分离`
`this.initValidate();`
`this.authState = "J1501";`
`//产生领域事件`
`publishEvent();`
`}`
`//授信成功`
`public void succ(){`
`}`
`//授信失败`
`public void fail(){`
`}`
`//只暴露Getter,因为一旦初始化不会改变`
`public Customer getCustomerId(){`
`return customerId;`
`}`
`//只暴露Getter,因为一旦初始化不会改变`
`public TenantId getTenantId(){`
`return tenantId;`
`}`
`}`
慎重选择抽象级别
- 软件模型是建立在一套业务行为方式的抽象上;
- 建模语言由领域专家表达;
- 软件模型要匹配领域专家的心智模型;
- 适当的抽象,防止太散就行组装会特别麻烦;
- 不要试图去解决无关紧要的无解问题;
- 不要预先满足未来的所有需求;
- 尽可能的控制范围,通过持续重构解决问题;
大小适中的聚合
要防止防止过度设计
- 聚合要设计的小巧(以满足业务为条件,不要过度);
- 聚合边界内保护业务不变性规则;
- 和领域专家确认,每个事件的响应时间(即时或延迟)
可测试的单元
聚合的设计要考虑可测试性。
可测试既可以保证业务的正确性,也可以保证后续持续重构的严重;
第6章 运用领域事件进行战术设计
领域事件是一条记录,记录这限界上下文中发生的对业务产生重要影响的事情;
注意点:
- 事件执行的顺序性(按业务逻辑必须保证);
- 事件消息丢失的问题;
设计、实现并运用领域事件
事件溯源
对所有发生在聚合实例上的领域事件进行持久化,把他们当做对聚合实例的记录;
通俗点讲,就是记录下log形成事件流,方便后续的追溯以及,事件的重试;也可以理解为流量回放;
要考虑存储的性能,以及快照恢复的时候;
第7章 加速和管理工具
主要介绍怎么加速DDD的设计和使用哪些工具或手段;
事件风暴
- 一种快速的设计技术;
- 领域专家和开发人员都可以参与到这个快节奏的学习过程;
- 聚焦业务和业务流程;
事件风暴步骤
- 通过创建一系列写在便利贴上的领域事件,快速梳理出业务流程;遵守的一些基本规则:
- 强调我们优先和主要关注的是业务流程;
- 把每个领域事件的名称卸载一张便利贴上;
- 把写好的便利贴按照时间顺序摆放在建模平面上(按照每个事件在领域中的发生的先后顺序从左到右排列)
- 按照业务流程,有些领域事件和其他事件并行发生,可以把这些事件摆放在同时发生的领域事件的下方;
- 在风暴讨论的这个步骤中,会在已有的或新的业务流程中发现问题点;
- 有时领域事件将导致一个需要执行的流程;
- 创建导致每个领域事件发生的命令;
领域事件由其他系统中所发生的事情引发,作为结果流入系统中。命令通常是某个用户操作的结果,命令的执行将导致领域事件的发生。
- 把命令和领域事件通过实体/聚合关联起来,命令在实体/聚合上执行并产生领域事件的结果。实体就是命令执行和领域事件触发的数据载体。
- 在建模平面上划出边界和表示事件流动的箭头连线。
- 识别用户执行操作所需的各种视图,以及不同用户的关键角色;
先以整体的视角画出核心流程,再以每一个节点深入梳理对应的具体逻辑。
在敏捷项目中管理DDD
运用SWOT分析法
想了解更多,请关注公众号