之前了解到微服务、中台等实践的背后都和领域驱动设计紧密相关,上周从图书馆借了2本相关书来读,记点笔记加深理解。
领域驱动设计
-
核心域: 限界上下文被当作组织的关键战略举措进行开发时即为核心域。是组织的核心竞争力,需要最好的资源投入。
-
A Big Ball of Mud/大泥球:指杂乱无章、错综复杂、邋遢不堪、随意拼贴的大堆代码。开发人员应时刻保持警惕,不断地通过设计和实现的优化来杜绝和延缓大泥球的形成。 演进式架构是一种有效手段,通过业务领域的划分不断地进行持续设计。
-
有效设计: 是简洁而非冗余,它也需要不断地演进与优化才能趋于完美。
“设计,让生活变得完美的艺术”是乔布斯的产品设计理念。首先意味着简洁的产品(开箱即用)、简洁的战略(产品的专注)和简洁的沟通(高效的沟通)。其次有效设计允许不完美。任何产品都始于不完美,只有通过不断试错和修正的迭代才可能逐步趋于完美。
战略设计
在展开具体实现细节之前,需要优先完成宏观层面的战略设计。它强调的是业务战略上的重点,如何按重要性分配工作,已经如何进行最佳整合。
DDD 主要关注的是如何在明确的限界上下文中创建通用语言的模型。
先确定核心域,专注于业务复杂性(而非技术复杂性)。
限界上下文
Bounded Context.
限界上下文是语义和语境上的边界。模型是在限界上下文中实现的,需要为每个限界上下文开发不同的软件。
通用语言
Ubiquitous Language.
可以理解为团队(开发、业务专家等)的国际语言,使得所有人能收到表达的准确含义和约束条件。
核心域
应当使用一组具体场景来表达核心域,描述领域模型该如何工作,各种组件该做什么。
可以通过实例化需求技术创建验收测试。
可能使用的架构:
- 端口和适配器/六边形架构
- 整洁架构/Clean Architecture
- 事件驱动架构、事件溯源
- 命令和查询职责分离/CQRS
- 响应式架构和Actor模型:每个Actor都可以看成一致性边界和一个独立的业务单元,和聚合对接。
- 具象状态传输/REST
- 面向服务的架构/SOA
- 微服务
CQRS: 区别于传统CRUD模式,把同一个模型的无副作用的查询操作和改变状态的修改操作(通常称为命令)分开。两部分分成不同的模块和服务实现。改变状态的命令经常会采用事件溯源来实现。 这种架构特别适合需要高性能且查询和命令的扩展性有不同需求的应用/服务。当然也会引入架构复杂性。
Service Oriented Architecture/SOA是一个组件模型,它将应用程序分为的不同功能单元(称为服务),通过这些服务之间定义良好的接口和契约联系起来。 实际上微服务和SOA一脉相承,可被认为是SOA的一种特定的现代实现。微服务相对SOA更注重对独立业务单元的拆分来形成清晰的边界,并采用轻量级的通讯机制,以一种更加松耦合的方式进行集成, 来提供更好的扩展性和灵活性。借助devops和云平台,微服务可以有单个独立的小团队开发、部署和运维。是现代分布式系统的首选架构,也是遗留架构首选的演进和重构方向。
子域
Subdomain. 处理遗留系统中无边界的复杂性.
子域是整个业务领域的一部分,负责为核心业务提供解决方案。限界上下文应该与子域一一对应。
项目中有三种主要的子域类型:
- 核心域/Sub Domain:核心业务功能
- 支撑子域/Supporting Domain:定制开发
- 通用子域/Generic Domain:可采购、外包或内部实现
上下文映射
Context Mapping. 集成多个限界上下文.
Context Map/上下文映射图同时定义了两个进行集成的限界上下文之间的团队间关系及技术实现方式.
上下文映射本质是一种集成关系,持续集成的实践活动对上下文映射关系不可或缺。
映射的种类:
- 合作关系/Partership:通过互相依赖的一套目标联合起来,需要联系紧密并经常同步
- 共享内核/Shared Kernel:共享小规模通用模型,常见方式是将通用模块以二进制依赖(Jar,链接库)方式共享
- 客户-供应商/Customer-Supplier:上下游关系。可采用消费者驱动契约(Consumer Driven Contract/CDC)实践,通过契约测试保持之间协作
- 跟随者/遵奉着/Conformist:上游过于强大,只能按约定集成
- 防腐层/Anticorruption Layer:下游团队在上下游通用语言模型之间创建翻译层。推荐实践。
- 开放主机服务/Open Host Service:REST服务是一种常见的实例
- 已发布语言/Published Language: 和开放主机服务结合,API+XML/JSON
- 各行其道/Separate Way
- 大泥球:特征有
- 越来越多的聚合因不合理的关联和依赖而交叉感染
- 对一部分进行维护就会牵一发而动全身,解决问题像在打地鼠
- 只剩“部落知识”和“个人英雄主义”,唯一“讲”出所有语言的极个别“超人”方能扶大厦之将倾
防腐层是最常见的一种阻止外部技术偏好或领域模型侵入的设计模式。API网关就是一种防腐层的具体实现。 另外对遗留单块系统进行拆分时,防腐层也发挥着巨大作用。 有一种对付单块系统的重构方式叫做“抽象分支/Branch by Abstraction”,其中从要拆分的模块中提取出的抽象层就发挥着防腐层的作用,在重构的过程中抵挡着未拆分部分对重构工作的腐蚀。
集成方式
- 基于SOAP的RPC:现代RPC机制更友好的支持多编程语言,序列化协议(gRPC,Thrift)比XML更高效,传输协议更现代(HTTP/2)
- RESTful HTTP: 避免直接把模型中的聚合暴露成资源,服务端提供的资源必须具有客户端需要的样子和组成
- 消息机制
- 领域事件由聚合发布,感兴趣的订阅方都可以消费
- 消息机制应支持至少一次投递/Reactive来保证所有消息最终都会被收到
- 订阅方必须实现幂等接受者/Idempotent Receiver
Backoff/退避算法重试机制,发送者会在再次重试之前等待一个随机时间,避免多个发送者按相同的时间间隔重试产生的冲突。等待时间的随机范围通常采用一种策略和算法来计算,常用的有指数退避算法。
战术设计
聚合
Aggregate模式.
- 值对象/Value Object/值:对一个不变的概念整体所建立的模型,常用来描述、量化或测量一个实体。
- 实体:一个实体模型就是一个独立的事物,拥有一个唯一的标识符,可以将它的个体性和其他实体区分开。
- 聚合:由一个或多个实体组成,其中一个实体被称为聚合根(Aggregate Root)。也可能包含值对象。
- 每个聚合的根实体(Root Entity)控制着所有聚集在其中的其他元素。根实体的名称就是聚合概念上的名称。
- 每个聚合都会形成保证事务一致性的边界。只能在一次事务中修改一个聚合实例并提交。
业务规则最终决定在单次事务完成提交后,哪些对象必须是完整、完全和一致的驱动力。
聚合的实现应避免贫血领域模型(函数式编程除外),先为聚合根实体创建一个类,它继承于基类Entity。
聚合设计的四条基本原则
- 在聚合边界内保护业务规则不变性
- 聚合要设计得小巧:符合SRP/单一职责原则
- 只能通过标识符引用其他聚合
- 使用最终一致性更新其他聚合。
领域事件
Domain Event.
领域事件是一条记录,记录着限界上下文中发生的对业务产生重要影响的事情。
领域事件类型的名称应该是对过去发生事情的陈述,即动词的过去式。比如ProductCreated,ReleaseScheduled.
领域事件通常由命令(一个方法/动作请求的对象形态)发布,命令可命名为CreateProduct。也可能由其他变化的状态引起,比如日期和时间。
消费事件的限界上下文要能识别出正确的因果关系,因果关系可以由领域事件类型本身表明,或由和领域事件关联的元数据表示,比如一个序列标识符或因果标识符。
事件溯源(Event Sourcing)
对所有发生在聚合实例上的领域事件进行持久化,把它们当做对聚合实例变化的记录。
可以在核心域上使用事件溯源,达到高吞吐量、低延迟和高伸缩性。性能最好的是缓存在内存中的聚合,另一种方式是使用快照(对聚合的增量状态的快照进行维护)。
辅助工具
事件风暴
使用Event Storming加速建模.
步骤:
- 通过创建一系列写在便利贴上的领域事件,快速梳理出业务流程。
- 创建导致每个领域事件发生的命令。
- 把命令和领域事件通过实体/聚合关联起来,命令在实体/聚合上执行并产生领域事件的结果。实体就是命令执行和领域事件触发的数据载体。
- 在建模平面上画出边界和表示事件流动的箭头连线。
- 识别用户执行操作所需的各种视图(View),以及不同用户的关键角色。
其他工具:
- 引入"Given/When/Then"格式编写的可执行的高级需求说明/验收测试/实例化需求
- 影响力地图/Impact Mapping
- 用户故事地图/User Story Mapping
敏捷项目管理DDD
- SWOT分析法(四象限矩阵)
- 优势/Strength: 领先于对手的业务或项目特征
- 劣势/Weakness: 落后于对手的业务或项目特征
- 机会/Opportunity:可以发挥项目优势的要素
- 威胁/Threat:存在于环境中并可能给业务或项目带来问题的因素
- 建模Spike和建模债务(类似技术债务)
Spike通过一系列的探索活动获取必要的知识,以降低技术方法的风险、更好的理解业务需求或提高用户故事的可靠性。 探索活动包括研究、设计、调查和原型等
- 任务识别和工作量估算
- 最简单和最准确的估算方式之一:基于度量指标的方法。
- 按组件类型为每个组件在表格增加一行
- 填入每一复杂级别(简单、适中、复杂)所需的小时数,估算含设计、实现、测试工作量
扩展阅读
- 《领域驱动设计精粹》
- 《实现领域驱动设计》
- 服务拆分与架构演进
- 《REST实战》
- “鱼变慢”还是“技术债”:适合国人口味的比喻