之前写了点随笔《微服务架构风格的DDD》,与朋友交流后,有些问题与想法,作些补充。
一、Q & A
- 【问】可否将《DDD》所说的Applicaiton Layer,Domain Layer 理解为两组不同的应用进程?例如有a, b, c 三个不同的应用程序属于Application Layer, 而d, e, f 三个不同的应用程序属于Domian Layer。a,b,c 依赖于 d,e,f 。
【答】不是不可以,但没必要这么理解。如果一个公司的业务很复杂,有大量的应用程序,需要对这些应用程序分组(划分子系统/ 划分系统 / 划分层次),可以按照更符合企业现状及发展规划的方式划分层次,例如: 前台系统、中台系统、后台系统,或者:前端应用层、编排层、共享服务层、核心层、主数据层, 或者:作业层、能力层、决策支持层、平台层。这也是《DDD》16.3章节所提到的大型结构Responsibility Layer 职责分层。面对大量的应用程序,参考公司所在的行业对应用系统分层,比简单的技术特征分层更有意义,没必要局限在Applicaiton Layer / Domain Layer /Infrastructure Layer 的划分。类似的 SaaS / PaaS / Iaas 也是技术特征分层,没有反映出公司所在的行业特征。 在单个应用程序里,理解Applicaiton Layer / Domain Layer /Infrastructure Layer的区别,对于架构设计及项目实施有意义。
- 【问】Android App 前端应用程序是否也可以按DDD划分 UI Layer / Applicaiton Layer / Domain Layer /Infrastructure Layer
【答】是的。 单独看前端应用(例如Android App),也可以很复杂,除了界面展示的部分,也会有缓存、也会有远程API调用,也会有手机硬件设备(摄像头、指纹)等的基础设施访问。也是一个独立的完整的应用程序,可以独立部署(安装)并运行。当然是可以按《DDD》的概念分层的。但客户端应用程序架构风格上与20年前传统的桌面应用程序架构风格差不了太多,只是运行的设备不同,体现在基础设施层有些差异。所以Android App按DDD概念分层在《DDD》这本书已经说得非常透彻,甚至个人认为有点啰嗦。前文主要想讨论对于微服务架构风格如何DDD。所以,从更高的层面上,将Frontend App与BFF App都看作是DDD分层的UI层。
- 【问】BFF 是属于UI层还是属于Application层?
【答】UI层。Backend For Frontend,顾名思义,服务于前端应用程序的后端应用程序,一个前端应用(包括不同平台的版本)通常对应于一个BFF。而一个前端应用,很可能是大杂烩,涉及多个领域,例如支付,理财,信贷,购物,都在一个前端App。这需要后端很多个领域的应用系统来支撑,这个BFF起到转发的作用,将支付,理财,信贷,购物等不同的请求代理到不同领域的系统。初看也有服务编排的作用,与Application Layer有些相似。但BFF并不聚焦于某个领域,而是与前端应用程序同步发展,可以认为它协调的是不同的Application Service。所以我个人倾向于将BFF归到UI层。
- 【问】《DDD》给出了哪些工具帮助拆分微服务应用?微服务应用应该拆分多大的粒度。
【答】我认为《DDD》中有两个比较重要的概念对解决这个问题有帮助:Ubiquitus Language、Bounded Context。Ubiquitus Language是要努力建设一份通用语言,这是在学习业务领域的专业知识,获得一致的理解。Bounded Context与Context Map则是要求找到业务领域的自然划分,与相互关联。Bounded Context概念还是有些抽象,能否顺利落地取决于对业务领域的认识有多深入。庖丁解牛就是最好的解释。先秦·庄周《庄子·养生主》:“依乎天理,批大郤,导大窾,因其固然”。顺着牛体的肌理结构,劈开筋骨间大的空隙,沿着骨节间的空穴使刀,都是依顺着牛体本来的结构。拆分微服务应用也是一样的,首重对业务领域的理解,再考虑团队结构等因素。更简单点的说,是遵循 “高内聚,低耦合”的基本原则。
二、领域对象的生命周期
- Aggregate
Aggregate聚合,实际是一组存在依赖(dependency)、关联(association)、组合(composition)、聚合(aggregation)这几种(UML里的)关系的对象集合。Domain Entity与Domain Primitive在内存中的存续期,是通过Aggregate聚合在一起的。
在设计初期,我们不一定能清晰的分析出Aggregate 的Root Entity,但如果遵循下面原则,则Root Entity会逐步浮现:
- 同一个Aggregate内,一个实体可以引用另一个实体,但不要轻易引用另一个实体的ID;
- 一些信息如果已经封装在Primitive中,则Entity应当引用Primitive,而不是单独引用Primitive里面的属性。
- Aggregate内的Entity与Primitive需要保障一致性规则,不能被其它Aggregate操作,可通过root entity作为Aggregate操作的出入口,只有root entity才能依赖于root entity。
- Factory
负责创建Entity与VO,并确保“创建即一致”,即所创建出来的对象在Aggregate范围内是符合一致性规则的。可以是Factory Method, Abstract Factory, 也可以是Builder 设计模式,也可以是Spring IOC的 BeanFactory, 甚至是 root entity的某一个方法直接通过new 创建出aggregate内的其它entiry。
VO不能有public setter,Entity对于会影响一致性规则的属性,应该尽量避免 public setter。
- Repository
很多Entity最终需要持久化存储,而不能长期存在于内存中。Repository负责持久化或者从持久化存储中查找并初始化。
- 一个领域模型定义的Repository数量一般与Aggregate数量差不多,Repository数量不与Domain Entity 一对一,一般远少于数据库的表的数量。
- Repository接口的入参、出参不应该使用底层数据对象(Data Object),而是使用Domain Module所定义的 Entity 或 VO。Repository 接口在Domain Module定义,看不见Repository Module所定义的 Data Object。这个也是为了避免底层实现逻辑渗透到业务代码中的强保障。
三、一些设计技巧
- Domain Primitive
【定义】在一个特定的业务领域里,准确的表达一个基础概念、可自我验证、拥有行为Value Object。
DP是 Value Object 的进阶版,在VO的immutable基础上,增加了 Validity 、行为,可以看作是这个业务领域里的基础数据类型。
【用途】
- Make Implicit Concepts Explicit (将 隐形的 概念 显性化)
- Make Implicit Context Explicit (将 隐形的 上下文 显性化)
- Encapsulate Multi-Object Behavior (封装 多对象 行为)
- Domain Event Publish-Subscribe
上一篇文章提到可以在Domain Model里定义一些Observer接口,当领域对象的状态发生变更时,或者Domain Service操作到某个点时,或者业务过程发生某个事件时,可以及时知会Application Layer。如果众多的Domain Entity或者Domain Service都需要实现Oberserver设计模式会是比较费力的。一种简单点的办法是实现一个通用的EventPublisher。但缺点是,这个通用的EventPublisher可能没有表示业务领域的概念却侵入到Domain Layer了。
Domain Event的发布与订阅通常是发生在同一个进程内的,事件的发布者并不关心事件的处理结果。事件的订阅者可以自行决定同步处理或异步处理,也可以再次发布到消息中间件或RPC。
- Specification Pattern 规约模式
某些业务场景可能特别依赖于大量业务规则,并且这些业务规则还是可灵活配置化的,这些场景可以考虑Specification Pattern。Specification Pattern在《DDD》中有详细描述,要求方法返回boolean,并且Specification对象可以 and / or / not 逻辑组合。
- 抽象第三方服务。
如果第三方服务可以抽象出一个接口,并且是当前领域的业务概念(Ubiquitus Language),则可以由领域层定义第三方服务的抽象接口,由基础设施层实现具体的第三方服务通信细节,由应用层将第三方服务的具体实现注入到领域层。这种方式与Repository接口类似。
网友评论