微服务架构风格的DDD

领域驱动设计的源头书籍

关于DDD能找到的最早的一本书是《领域驱动设计 软件核心复杂性应对之道》,2003年Eric Evans 著,Martin Fowler作序。是的,你没看错,是差不多20年前的了。所以DDD不是什么新技术,而是老技术,但历久弥新,即使时代变迁,今天依然是经典。另一本《实现领域驱动设计》太厚了,转而学习同一作者[美] Vaughn Vernon 著的《领域驱动设计 精粹》。但感觉 Vaughn Vernon所理解的未必完全是 Eric Evans所表达的。 截图Eric Evans 的概念,后面再结合我自己的经验理解。

一、Layered Architecture分层定义

《领域驱动设计 软件核心复杂性应对之道》分层访问关系

《领域驱动设计 软件核心复杂性应对之道》分层定义

二、 时代变迁了哪些?

从上面的图示可以看出,20年前基本是桌面应用的时代,web应用也没有非常广泛,更不用说现在的云时代+移动时代。虽然现在依然流行桌面应用,但作为更广泛的业务应用系统,这里只讨论新时代下基于微服务架构风格如何应用DDD及20年前的不同点。

基于微服务架构风格的应用组件
  1. 前后端分离。云+移动的时代,注定了前后端需要分离。单独看前端应用(例如Android App),也可以很复杂,除了界面展示的部分,也会有缓存、也会有远程API调用,也会有手机硬件设备(摄像头、指纹)等的基础设施访问。也是一个独立的完整的应用,也可以单独讨论DDD。但客户端应用架构风格上与桌面应用也差不多,不是这里讨论的主题。从更高的层面上,这里将frontend app与BFF app都看作是DDD分层的UI层。在Naresh Bhatia的文章中,认为SecurityWebService、TradingWebService 、MarketPriceListener 、ExchangeMessageListener 都属于UI层。我不太认同。我认为MarketPriceListener 、ExchangeMessageListener 属于Application Layer,而SecurityWebService、TradingWebService 一般在BFF里,属于UI层。是的,我将UI层从微服务架构风格的后端应用(下文简称“微服务应用”)里踢出去了,归到前端与BFF,微服务应用里只剩下三层。
  2. UI层不能再访问基础设施层,不能再访问领域层。参照图1各层访问关系,但其它各个箭头表达的访问关系仍然是成立的。
  3. 从分层定义来看,基础设施层,不再为用户界面层绘制屏幕组件。其它各层定义,依然成立。

三、 各层都包括了哪些?

1. UI Layer

  • web app: html5 / css / js / img/ other static resource files
  • 客户端应用:android App / iOS App, 微信小程序,支付宝小程序。
  • 前端SDK:方便别家的APP集成的。
  • BFF : Backend For Frontend , maybe based on node.js or other.

2. Application Layer:

可以协调Domain Service ,访问Domain Layer,作为IOC 或Bean Factory 装配各种Bean (一般由Spring 代劳了,但java Configuration 应当在此层)。 可以调用RPC ,监听消息队列。

  • Application Service 。可以是各种rpc协议的接入服务。也可以是基于restful 的http接入服务(也可能是基于Spring MVC的输出 json的Controller)。
  • Message Listener 实现类。接收MQ中间件的消息然后编排服务,协调相关资源,调用领域层对象。
  • 部分定时跑批作业,如果没有独立部署的,也属于应用层。关于跑批作业的完成进度,也在这一层。
  • Value Objects. 为了方便RPC传输的值对象。如果Domain Entity序列化性能也不差,则不一定需要VO,有时候Domain Entity不适合序列化传输(例如作为Aggregate Root ),则需要定义更合适的VO。
  • Observer实现类: Business Lifecycle Callback Interface (Observable定义的接口)的具体实现类。一般负责监听并处理Business Lifecycle 的Domain Event。可以异步发送MQ,也可以同步调用RPC。Message Listener实现类是从外到内,从Application Layer到 Domain Layer;Observer实现类是从内到外,从Domain Layer到 Application Layer。
  • Spring IOC, 是关于各处Bean的依赖注入、装配,是负责应用生命周期内各组件的依赖关系的。

3. Domain Layer:

领域层实际是Domain Business Model 领域业务模型,不是简单的数据模型,不能理解为E-R图中的E。这一层实际也是业务应用系统中六边形架构的核心。这个模块作为Artifact只需要Mock Repository接口,而不需要依赖于任何第三方库、第三方框架、中间件(Servlet容器、MQ中间件、DB服务器),就可以单独测试,并且验证当前Domain所有业务逻辑的正确性。如果某一个Class需要依赖于除了JDK之外的框架(log日志框架例外)才能编译通过或者单元测试,则这个Class很可能不属于Domain Layer。

  • Domain Entity :必须是富血模型,必须是业务模型里涉及的隐式或显示的概念,而且必须是有唯一标识(不一定是数字)。在这个Entity存续期内,其标识不能被修改。富血模型要求真正的面向对象,不只是有属性、setter与getter,还应该有对应的行为。应该尽可能的富血模型而不是贫血模型。如果某个function不依赖于外部而只依赖于对象内部的属性或者方法,则应该定义在Entity,不应该抽离到Utils也不应该抽离到Domain Service。

  • Value Objects: An object that represents some descriptive aspect of the domain, but has no conceptual identity is called a Value Object. A good example of a value object is Money. 必须是业务模型里涉及的隐式或显示的概念,一般没有明确的可追踪的ID标识。

  • Domain Event . 业务逻辑处理过程(Business Lifecycle)中发生的事件。不包括技术上的 Error or Exception or RuntimeException。如果一个外部MQ消息与当前领域模型有关,也可以包装成Domain Event,但一般在Application Layer优先考虑将MQ消息转化为Domain Entity / VO。

  • Observer Interface / Business Lifecycle Callback Interface . 可以在这一层定义观察者设计模式中的Observer接口,这些接口方法的形参可以有Domain Event,也可以没有Domain Event。通常Application层会实现具体的Observer,注入到这个领域层。领域层不能依赖于应用层,但可以通过回调接口的方式,间接调用应用层的代码。Application Layer也可以通过Callback Interface形成特定事件,然后发送到MQ中间件。

  • Domain Service :Some aspects of the domain are not easily mapped to objects. The best practice is to declare these as Domain Services. 一些不适合映射到领域对象里的过程,可以声明为领域服务。 Domain Service一般是无状态的单实例,可以用于协调涉及多个Entity的业务逻辑。所有的public的class / method / member必须是业务团队能理解的平时会说的名词或动词(统一语言)。如果某个方法名用的是技术名词而业务团队不能理解,则可能不适合放在Domain Model,而可能需要放在Infrastructure Layer或者 Application Layer。

  • Repository Interface: REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样。FACTORY负责处理对象生命周期的开始,而REPOSITORY帮助管理生命周期的中间和结束。FACTORY和REPOSITORY具有完全不同的职责。FACTORY负责制造新对象,而REPOSITORY负责查找已有对象。这个接口的出参、入参必须是这层的Domain Entity或 Value Object,不能是DTO。Repository Interface 可以被Domain Service所调用,但不能被Domian Entity所调用。Repository Interface需要屏蔽基础设施的细节。领域业务模型决定了数据逻辑模型,数据逻辑模型决定了数据库物理模型。

4. Infrastructure Layer:

  • ORM(Object Relational Mapper)。

  • RPC Client. 微服务架构风格的应用都会存在大量的RPC调用。但每一个Domain都应该保持领域自治的特性,Domain Service不应该直接调用RPC。必要时可以由Application Service调用RPC转换为当前Domain的Enitity与VO递交给Domain Service。如果某个Domain Service 需要直接调用RPC,则应当考虑设计是否正确。在一次(Trace)调用链的所有RPC调用应当是符合DAG(以Application为节点的有向无环图),一旦违反DAG则应当重新考虑关于Domain Bounded Context 、 Application Service的定义、Application交互关系的设计是否正确。如果是基于Dubbo框架或者Hessian实现RPC,则相应的Skeleton属于这个RPC Client。如果是基于Thrift或者ProtoBuf 或者gRPC,则相应的IDL及生成的客户类库属于这个RPC Client。

  • MQ Helper. 与具体MQ技术细节相关的class 属于 Infrastructure Layer,但一般的Domain Layer 并不知道MQ的存在,中间是通过Applicarion Layer协调的。Application Layer可以 向Domain Layer注册观察者,监听特定的Domain Event,然后调用message producer发送。Application Layer可以实现Message Listener,注入到Infrastructure Layer的MessageConsumer, 监听MQ的消息,当onMessage时在Message Listener具体实现类中转换为Domian Entity / Value Object / Domain Event, 转交给Domain Service进行业务逻辑处理。

  • Servlet Container. 等负责底层协议的。

四、FAQ

1. Repository 属于Domain Layer 还是Infrastructure Layer?

阿里殷浩认为Repository的接口是在Domain层,但是实现类是在Infrastructure层。我也认为如此。Eric Evans 提到“REPOSITORY应该让客户感觉到那些对象就好像驻留在内存中一样”,所以Repository Interface 表达的仍然是当前Domain 的业务逻辑。但在具体实现时,这往往有些争议,因为要实现这个Repository Interface,implementation class 肯定要依赖于更底层的DB Driver或ORM,并且屏蔽技术细节。例如业务刚发展时性能需求不高,后来用户量激增,需要缓存了,单个关系型数据库不能满足了,于是用了Redis作缓存,部分数据可能又改为了MongoDB做NoSQL文档存储。假设Domain Model 做成一个单独的Artifact,为了保持其更纯粹的业务模型不被技术细节与底层设施所侵蚀,一般不希望这个artifact依赖于mybatis或Redis client。在六边形架构或洋葱圈架构,可以让这个infrastructure artifact 依赖于 domain model artifact。现在普遍流行MyBatis,可以认为mybatis mapper的xml配置文件属于Infrastructure Layer,是Repository Implementation。

2. 有哪些Artifact?构建时是怎样的依赖关系(maven depend)

  • XxxApplication: denpend XxxDomain, XxxRepository, XxxRpcClient,XxxMQHelper
  • XxxDomain: 无依赖,
  • XxxRepository: depend mybatis, XxxDomain , Kkk-JDBC-Driver
  • XxxRpcClient: depend rpc基础类库。
  • XxxMQHelper: depend Kkk-MQ-Driver

3. 六边形架构关于输入输出的依赖

XxxDomian作为六边形架构的核心,不会直接依赖于外部。但

  1. 定义了Oberserver接口用于间接输出Domain Event到MQ,相当于定义了一个六边形架构的输出端口;
  2. 定义了Repository接口用于间接持久化到数据库或者缓存,相当于定义了一个六边形架构的输出端口;
  3. 定义了DomainService接口用于接受外部的rpc输入,或者接受MessageListener消息输入,相当于定义了一个六边形架构的输入端口。

具体的MessageListener实现类,Application Service 实现类,将外部的消息/rpc适配给到Domain Service,并协调相关资源装配相关流程。Observer具体实现类、Repository具体实现类作为适配器,由Applicaiton Layer注入到Domain Layer ,(当然实际Java项目有时候由Spring IOC代劳了一部分工作)。
Domain 不关注任何的技术框架与具体的技术细节,不关注任何的输入输出设施,只专注于业务逻辑,并且清楚的知道业务的生命周期,在流程的重要环节设置了接口,具有很大的可扩展性。如果人力有限,只能逐个Artifact开发,那么XxxDomain必须是第一个。

五、参考

  1. 《领域驱动设计 软件核心复杂性应对之道》[美]Eric Evans 著
  2. 《技术人的百宝黑皮书》阿里技术专家详解 DDD 系列(作者:殷浩)
  3. 《领域驱动设计 精粹》[美] Vaughn Vernon 著
  4. archfirst.org/domain-driven-design Naresh Bhatia
  5. 殷浩详解DDD系列 第三讲 - Repository模式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容