领域驱动设计(DDD)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型。
————《实现领域驱动设计》
一、战略建模
1,领域
领域即是一个组织所做的事情以及其中所包含的一切。领域可以表示整个业务系统,也可以表示其中的某个核心域或支撑子域。
一个例子,零售商领域可以分为4个子域:产品目录,订单,发票,物流,他们组成一个电子商务系统,外部还包括库存、外部预测系统两个子域。
2,限界上下文
一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。
一个给定的业务领域会包含多个限界上下文,想与一个限界上下文沟通,则需要通过显示边界进行通信。系统通过确定的限界上下文来进行解耦,而每一个上下文内部紧密组织,职责明确,具有较高的内聚性。
3,上下文映射图
比较容易的一种是画一个简单的框图来表示两个或多个限定上下文之间的映射关系。另一种方式是集成限界上下文,这种方式后面会介绍。
限界上下文之间的映射关系
合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
发布语言(Published Language):通常与OHS一起使用,用于定义开放主机的协议。
大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
另谋他路(SeparateWay):两个完全没有任何联系的上下文。
在上下文映射图中,我们使用一下缩写来表示各种关系:
ACL表示防腐层
OHS表示开放主机服务,
PL表示发布语言
U表示上游
D表示下游
这是一个上下文映射图的例子:
二、战术建模
1,实体
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。
例如最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。
2,值对象
当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。
例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。
值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实体。使用值对象,可以更好地做系统优化、精简设计。
它具有不变性、相等性和可替换性。
在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。
在实践中,我们发现虽然一些领域对象符合值对象的概念,但是随着业务的变动,很多原有的定义会发生变更,值对象可能需要在业务意义具有唯一标识,而对这类值对象的重构往往需要较高成本。因此在特定的情况下,我们也要根据实际情况来权衡领域对象的选型。
3,聚合根
Aggregate(聚合)是一组相关实体和值对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。
聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值,可以指导详细设计。
4,贫血症和失忆症
贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有行为和动作的领域对象。
对象是面向对象语言的核心,而对象是将数据和行为封装在一起的。如果一个对象只是数据的载体,没有行为,只有简单的get和set方法,那么这个对象就具有贫血症。
按照我们通常思路实现,我们的业务逻辑都是写在Service中的,而各种封装数据的对象充其量只是个数据载体,没有任何行为。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。
更好的是采用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。
5,如何创建好的聚合?
边界内的内容具有一致性:在一个事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独立的聚合,采用最终一致的方式。
设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使一定要包含,可以考虑将其创建为值对象。
通过唯一标识来引用其他聚合或实体:当存在对象之间的关联时,建议引用其唯一标识而非引用其整体对象。如果是外部上下文中的实体,引用其唯一标识或将需要的属性构造值对象。 如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。
6,领域事件
表示领域模型中发生的重要事件。有多种方式可以对领域事件建模。在对聚合进行命令操作是,聚合本省将发布领域事件。
7,模块
模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。
在代码中,一般的工程中包的组织方式为{com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部,我们可以将模块看成这种包结构。模块中包含的领域对象应该是内聚在一起的。
8,资源库
领域对象需要资源存储,存储的手段可以是多样化的,常见的无非是数据库,分布式缓存,本地缓存等。资源库(Repository)的作用,就是对领域的存储和访问进行统一管理的对象。
9,领域服务
一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。
当一些领域业务逻辑,既不适合放在实体、值对象、聚合中是,应该将其放在领域服务中。
上文中,我们将领域行为封装到领域对象中,将资源管理行为封装到资源库中,将外部上下文的交互行为封装到防腐层中。此时,我们再回过头来看领域服务时,能够发现领域服务本身所承载的职责也就更加清晰了,即就是通过串联领域对象、资源库和防腐层等一系列领域内的对象的行为,对其他上下文提供交互的接口。
当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。如原本由聚合根暴露的业务逻辑也需要依托于领域服务。
10,上下文集成
通常集成上下文的手段有多种,常见的手段包括开放领域服务接口、开放HTTP服务以及消息发布-订阅机制。
三、架构
DDD的一大好处便是它不需要使用特定的架构。由于核心域位于限界上下文中,我们可以在整个系统中使用多种风格的架构。有些架构包围着领域模型,能够全局性的影像系统,而有些架构则满足了某些特定的需求。我们的目标是选择合适自己的架构和架构模式。
微服务架构众所周知,此处不做赘述。我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。
以下介绍一些常见的微服务架构:
1,分层架构
分层架构模式被认为是所有架构的始祖,它支持N层架构系统,因此被广泛的应用。在这种架构中,我们将一个程序或者系统分为不同的层次。
下图为一个典型的DDD系统所采用的分层架构:
用户界面只用于处理用户显示和用户请求,它不应该包含领域或业务逻辑。用户可能是人,也可能是其他系统,此时用户界面层采用OHS(开放主机服务)的方式向外提供API。
应用服务位于应用层,应用服务是很轻量的,它本身不处理业务逻辑,主要用于协调对领域对象的操作,比如聚合。
领域服务位于领域层,主要处理该领域的业务逻辑。
基础设施层主要实现对资源库的访问。
分层架构一个重要的原则是:每层只能与位于其下方的层发生耦合。
分层架构的好处是显而易见的。
首先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,而不必关心其他层的设计,也不必担心自己的设计会影响其它层,对提高软件质量大有裨益。其次,分层架构使得程序结构清晰,升级和维护都变得十分容易,更改某层的代码,只要本层的接口保持稳定,其他层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。
2,六边形架构(端口与适配器)
六边形每条不同的边代表了不同的类型的端口,端口要么处理输入,要么处理输出。
这种架构中,不同的客户通过“平等”的方式与系统交互,新增客户只需要添加一个新的适配器将客户端的输入转换成能被系统API所理解的参数就行。同时系统输出,也通过不同的适配器转化。
六边形架构中,内部业务逻辑(应用层和领域模型)与外部资源(APP,WEB 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错的主要问题,从而可以很好的实现前后端分离。
六边形架构包含两层
- 代表传达机制和基础设施的外层;
- 代表业务逻辑的内层。
3,洋葱架构
洋葱架构与六边形架构有着相同的思路,它们都通过编写适配器代码将应用核心从对基础设施的关注中解放出来,避免基础设施代码渗透到应用核心之中。这样应用使用的工具和传达机制都可以轻松地替换,可以一定程度地避免技术、工具或者供应商锁定。
另外,它还有着脱离真实基础设施和传达机制应用仍然可以运行的便利,这样可以使用 mock 代替它们方便测试。
然而,洋葱架构还告诉我们,企业应用中存在着不止两个层次,它在业务逻辑中加入了一些在领域驱动设计的过程中被识别出来的层次:
洋葱架构的关键原则:
围绕独立的对象模型构建应用
内层定义接口,外层实现接口
依赖的方向指向圆心
所有的应用代码可以独立于基础设施编译和运行
—— Jeffrey Palermo 2008, The Onion Architecture: part 3
还有,任何一个外部层次都可以直接调用任何一个内部层次,这样既不会破坏耦合的方向,也避免了仅仅为了追求分层模式而创建一些没有任何业务逻辑的代理方法甚至代理类。这和 Martin Flowler 表达的偏好一致。
洋葱架构在端口和适配器架构的基础之上增加了一些的应用业务逻辑的内部组织,这些组织基于领域驱动设计的概念划分的。
4,CQRS(命令与查询职责分离)
CQRS是对CQS模式的进一步改进成的一种简单模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。“CQRS只是简单的将之前只需要创建一个对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的(这个和CQS的定义一致)”。
CQRS使用分离的接口将数据查询操作(Queries)和数据修改操作(Commands)分离开来,这也意味着在查询和更新过程中使用的数据模型也是不一样的。这样读和写逻辑就隔离开来了。
这个例子是一个简单的在线记日志(Diary)系统,实现了日志的增删改查功能。整体结构如下:
上图很清晰的说明了CQRS在读写方面的分离,在读方面,通过QueryFacade到数据库里去读取数据,这个库有可能是ReportingDB。在写方面,比较复杂,操作通过Command发送到CommandBus上,然后特定的CommandHandler处理请求,产生对应的Event,将Eevnt持久化后,通过EventBus特定的EevntHandler对数据库进行修改等操作。
查询模型是一种非规范化数据模型,它不反映领域行为,只用于数据查询和显示;命令模型执行领域行为,在领域行为执行完成后通知查询模型。
命令模型如何通知到查询模型呢?如果查询模型和领域模型共享数据源,则可以省却这一步;如果没有共享数据源,可以借助于发布订阅的消息模式通知到查询模型,从而达到数据最终一致性。
Martin 在 blog 中指出:CQRS 适用于极少数复杂的业务领域,如果不是很适合反而会增加复杂度;另一个适用场景是为了获取高性能的查询服务。
对于写少读多的共享类通用数据服务(如主数据类应用)可以采用读写分离架构模式。单数据中心写入数据,通过发布订阅模式将数据副本分发到多数据中心。通过查询模型微服务,实现多数据中心数据共享和查询。
四、设计领域模型的一般步骤
设计领域模型的一般步骤如下:
- 根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;
- 进一步分析每个上下文内部,识别出哪些是实体,哪些是值对象;
- 对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
- 为聚合根设计仓储,并思考实体或值对象的创建方式;
- 在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构。
五、DDD和MVC的对比
六、参考文献
1,《实现领域驱动设计》Vaughn Vernon著;
2,http://deepoove.com/blog/#/posts/69(领域驱动设计DDD和CQRS落地)
3,https://www.edjdhbb.com/(复杂度应对之道 - 阿里的COLA应用架构)
4,https://www.jianshu.com/p/d87d5389c92a(洋葱架构)
5,https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html(浅谈命令查询职责分离(CQRS)模式)
6,https://www.jianshu.com/p/d3e8b9ac097b(清晰架构(01): 融合 DDD、洋葱架构、整洁架构、CQRS…(译))
7,02.DDD、业务中台化、微服务设计——理论篇
8,https://github.com/Sayi/ddd-cargo(GitHub的领域驱动设计项目实战)