DDD理念探索

当前困境

        随着公司规模的不断扩大,业务程序变得越来越复杂,代码可读性、复用性和扩展性很差,技术优化无从下手,代码改动牵一发而动全身,业务推进困难,大量系统重复建设等等问题层出不穷。为了解决此类问题我们也尝试过不少的方式方法,例如加强设计评审,定义代码规范、代码评审等等用以提高系统以及代码质量。然而真正执行推进时我们发现代码还是太复杂、太难维护,同时大家对业务背景的理解程度不同,认知不同,沟通低效,在做评审时也很难找出代码、系统设计上的不合理性,我们几乎很难将代码与设计对应起来,代码无法正确的反应设计,代码评审也就仅仅变成了代码规范性、代码设计优雅度的评审,很难找出代码业务逻辑的问题。

         那在软件开发中该如何才能提高系统的可维护性、如何降低软件系统复杂度?(本篇文章作为 DDD 设计思想的入门引导篇,尝试利用DDD思想指导系统设计以降低系统复杂度。)

Session 1   为什么DDD能够解决软件复杂性

一、软件复杂性的根源

BookModel 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 BookService 中。我们通过 BookService 来操作 BookModel。换句话说,Service 层的数据和业务逻辑,被分割为Model 和 Service 两个类中。像 BookModel 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。

这类风格代码是不是看起来也很眼熟?我们日常使用的mvc三层开发代码结构里也充斥着此类代码。后端项目分为 Repository 层、Service 层,Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口,业务规则代码与数据库访问、关联对象数据库访问、结果处理等其它逻辑在一起实现,通过代码还原业务规则会越来越复杂且随着时间推移,代码逻辑会越来越偏离设计。

这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。贫血模型下通过阅读业务逻辑层的代码来还原真实的业务规则很困难,很难从代码反映其业务规则设计,并且随着软件需求变更,业务规则更加难以还原,软件复杂度将不可控。

所以,贫血模型软件是复杂性的根源。

二、DDD是如何解决软件复杂性的?

在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Entity 类两部分。Entity 就相当于贫血模型中的 Model。不过Entity 与 Model的区别在于它是基于充血模型的,既包含数据,也包含业务逻辑。

我们日常的开发也就是贫血模型的MVC开发模式,大部分都是 SQL 驱动(SQL-Driven)的开发模式,我们先设计数据库,定义Model,然后模板式地往对应的 Repository、Service、Controller 类中添加代码。

业务逻辑与数据库操作一一对应,而数据库操作基本都是针对特定的业务功能编写的,复用性差。当要开发另一个业务功能的时候,只能重新写个满足新需求数据库操作。在这个过程中,也很少有人会有代码复用意识。这种方式对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的开发方式会让代码越来越混乱,最终导致无法维护。

而在应用基于充血模型的 DDD 的开发模式下,开发流程发生了很大的改变。我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。越是复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期设计上,更需要我们做好业务调研、领域模型设计。

所以,基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。

Session 2  DDD理念

一、DDD是什么?

DDD(domain driven design)领域驱动设计。核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。DDD专注于从业务领域出发,将大的业务领域分解为小的子域,完成领域建模,用领域模型指导服务设计和落地。也就是说采用分而治之的策略来降低业务领域认识和软件产品建设的复杂度。

DDD与传统软件设计思想:

面向过程编程(POP),多应用于单机架构,系统包括客户端 UI 层和数据库两层,采用 C/S 架构模式,整个系统围绕数据库驱动设计和开发,并且总是从设计数据库和字段开始,并把接触到的需求功能拆解成一个个函数方法,以此分解软件的复杂度。这适用于软件的复杂度不是很大的系统。

面向对象编程(OOP),多应用于集中式架构,也是当前的主流架构,系统包括业务接入层、业务逻辑层和数据库层,大多采用经典三层mvc架构。采用这种设计思想会把接触到的需求首先分解成一个一个对象,然后给每个对象添加一个一个方法和属性,程序通过各种对象之间的调用以及协作,同样也是围绕着数据库设计,从而实现计算机软件的功能。随着业务发不断地发展变化,系统同样也会变得臃肿,可扩展性和弹性伸缩性差。

领域驱动设计(DDD),分布式微服务架构,接触到需求后首先把需求分解成一个一个问题域(自上而下),然后再把每个问题域分解成一个一个对象,程序通过各种问题域之间的调用以及协作,从而实现计算机软件的功能。该理念下的微服务架构可以很好地实现应用之间的解耦,解决单体应用扩展性和弹性伸缩能力不足的问题。

DDD 与微服务:

DDD 是一种架构设计方法,不是架构而是一种架构设计方法论,指导微服务拆分与设计。

DDD 从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。微服务更多的关注运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,服务的独立开发、测试、构建和部署。

两者从本质上都强调从业务出发,都是为了追求高响应力,从业务视角去分离应用系统、降低复杂度的手段,核心要义是要合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力。

DDD与敏捷迭代:

敏捷宣言宣称,“不断关注优秀的技能和好的设计会增强敏捷能力”。好的设计不是正确地预测了未来的设计,而是让适应未来的成本不那么高昂的设计。敏捷的目的不只是速度,而是敏捷性,所以设计是不能忽视的。

然而许多在实行中的敏捷的团队只强调MVP(最小可行产品)的重要性,不重视设计。系统架构草草设计、代码开发只为实现当前功能,陷入系统逐渐繁琐失去生机的泥潭。而使用DDD 理念从业务领域出发分而治之就可以较好的克服这一问题,所以敏捷与DDD 的结合可以加速软件交付。

在敏捷的先启(Inception)阶段,运用DDD的战略设计方法,建立初步的统一语言,在识别出主要的史诗级故事与主要用户故事之后,进而识别出限界上下文,此时再建立系统的逻辑架构与物理架构。开发者每一个功能的实现、每一行代码的编写都是围绕着用户故事开展,这也是构成领域知识的最基本单元。在迭代周期内团队首先要深入分析用户故事,了解本次迭代的目标,对迭代中的每个任务要建立基本的领域知识的理解,这直接指导着开发人员的开发、测试人员的测试。

二、基础概念

DDD 的核心知识体系,具体包括:领域、子域、核心域、通用域、支撑域、限界上下文、实体、值对象、聚合和聚合根等概念。

2.1 领域、子域、核心域、通用域和支撑域

领域用来确定范围。在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。

例如对于小说阅读领域,我们可以把小说阅读细分为看书、评论、用户等子域,而用户子域还可以继续细分用户信息、福利、消息等子子域。

子域可以根据重要程度和功能属性划分为如下:

  • 核心域:决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力,例如小说阅读领域中的看书。
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能的子域,例如用户子域。
  • 支撑域:但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,例如评论子域。

核心域、支撑域和通用域的主要目标:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。因此,在公司建立领域模型时,我们就要结合公司战略重点和商业模式,重点关注核心域。

2.2 通用语言和限界上下文

通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。

  • 通用语言:就是能够简单、清晰、准确描述业务涵义和规则的语言。
  • 限界上下文:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

2.2.1 通用语言

通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。它可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,这也统一产品和研发的话术,从而确保业务需求的正确表达。在DDD战略设计的事件风暴过程中(后面会再讲到),通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。

通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。其实就是把领域对象、属性、代码模型对象等。

2.2.2 限界上下文

通用语言也有它的上下文环境,为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

限界上下文是一个显式的语义和语境上的边界,领域模型存在于边界之内。边界内,通用语言中的所有术语和词组都有特定的含义。把限界上下文拆解开看,限界就是领域的边界,而上下文则是语义环境。所以限界上下文很自然的成为了划分领域的重要依据。

例如某宝上的七猫周边商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。

2.3 实体和值对象

2.3.1 实体

实体一般是指能够独立存在的、作为一切事物本原的东西。实体一般对应业务对象,它具有业务属性和业务行为。DDD中实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。

例如书籍是小说阅读上下文的一个实体,通过唯一的书籍 ID 来标识,不管这个书籍的数据如何变化像是分类标签等变化,书籍的 ID 一直保持不变,它始终是同一本书。

而值对象主要是属性集合,对实体的状态和特征进行描述。

2.3.2 值对象

值对象是通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。值对象本质上就是一个集合,在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。

例如用户实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。如果将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,那这个集合就是值对象了。

2.4 聚合和聚合根

2.4.1 聚合

我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现,例如阅读中的书、章两个实体的关系;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务,例如书籍阅读以及书籍签约两个服务。

2.4.2 聚合根

如果把聚合比作组织,那聚合根就是这个组织的负责人。**聚合根也称为根实体,它不仅是实体,还是聚合的管理者。**在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。

例如书籍阅读领域中书籍就是聚合根,管理着章节等其他实体。

2.5 领域事件

领域事件用来表示领域中发生的事件。

例如小说阅读用户购买会员下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。

2.6仓储

仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

Session3  DDD实战设计

一、 DDD落地三步走

DDD 思想落地过程可简单归纳为三个阶段:1业务分析 →2战略设计→3战术设计

业务分析阶段为战略设计输出经过统一语言描述的业务事件、业务逻辑以及业务分类,而战略设计阶段又为战术设计阶段输入领域模型以及边界上下文,方便其进行微服务拆分以及模型映射。

1、业务分析:在这个阶段需要集齐项目团队的成员主要包括领域专家、设计人员、开发人员等一起对业务问题域以及业务期望进行全面的梳理,厘请业务中的统一语言,在业务领域中发现领域事件、领域对象及其对应的领域行为,搞清楚他们各自的关联关系。

2、战略设计:对业务进行领域划分构建领域模型,梳理出相应的限界上下文,通过统一的领域语言从战略层面进行领域划分以及构建领域模型。在构建领域模型的过程中需要梳理出对应的聚合、实体、以及对象。

补充:事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。

3、战术设计:以领域模型为战术设计的输入,以限界上下文作为微服务划分的边界进行微服务拆分,在每个微服务中进行领域分层,实现领域模型对于代码的映射。

通过战略设计,建立领域模型,划分微服务边界。通过战术设计,从领域模型转向微服务设计和落地。此时,边界清晰、可持续演进的微服务架构雏形已然出现。

二、 DDD 分层架构

2.1 层级介绍

在DDD战术设计阶段,用DDD的分层架构实现微服务内各层之间解耦,很好的降低各层之间的依赖。在发生变化时,可以降低各层之间相互影响,保证各层模型的稳定。

  • 用户接口层:处理前端发送的请求和解析用户输入的配置文件等,将数据传递给应用层,或者获取应用服务的数据后,进行数据组装,向前端提供数据服务。
  • 业务应用层:用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。
  • 领域层:实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。
  • 基础设施层:1为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;2对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现。

2.2 代码模型总目录结构

据上我们基本得出一下代码层级结构

2.3各层数据对象的职责和转换过程

视图对象 VO(View Object),用于封装展示层指定页面或组件的数据。

数据持久化对象 PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。

领域对象 DO(Domain Object),微服务运行时的实体,是核心业务的载体。

数据传输对象 DTO(Data Transfer Object),用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。

2.3  DDD与CQRS的落地组合拳

CQRS全名Command-Query Responsibility Segregation。CQRS是一种与传统的DDD分层架构实现不同的模式,将写与读区分开。

在现实的DDD实践过程中会遇到查询不可能只查询某一个领域对象这么简单,很多时候是复杂的关联多个领域对象的查询,而且这种组合查询可能涉及到复杂的领域关系,再以作者书籍签约为例,此时产品想要在做一个书籍签约信息大列表页展示有书籍信息作者信息以及签约信息,那这个列表就包含了多个领域数据书籍领域、签约领域甚至作者用户领域。而常规DDD的实体设计将方法和属性都归于一个类中的做法无法应对这种场景。如果硬套用会导致业务逻辑的泄露,反倒变成了一个大泥球了。而基于DDD设计思想的CQRS架构实践落地正是解决这种复杂场景的一个方法。

其实复杂系统中高效的查询数据结构一定要经过ETL清洗过程。CQRS的思路其实参考了这个想法,领域对象实体中的方法都是服务当前领域对象自身的,主要集中在对象状态的更新和获取方面,这也就是上图中的命令处理器流程。而跨领域对象的查询从业务领域角度出发可以抽象出单独的领域服务,而这些领域服务主要的作用是组合多个业务对象返回业务需要的查询结果,这也就是上图中的查询处理器的作用。这个需求既体现了单独的业务需求如果没有一个可以抽象出来的业务实体与之对应,那么领域服务就该在此刻登场了。以一种服务的形式提供给应用服务来响应前端的请求。CQRS巧妙的利用了领域事件机制,让更新和查询两个过程的相关数据保持最终一致性,而且有效避免了数据仓库的ETL的漫长数据处理等待过程。

其实CQRS的架构思想可以简单也可以复杂。例如数据存储的形式可以有几种形式:共享数据库共享表;共享数据库分表;分数据库。应用程序也可以是单一的或者多个微服务结合。

2.4调整后的工程目录

DDD不仅适合微服务,也适合传统架构,在单体内部可以划定聚合和业务边界,从而提高扩展性,可维护性,解藕业务之间的强依赖关系,如此即便是后期想要拆分服务也很方便,只要将领域服务代码模块拆分出去单独部署即可。

图示便为DDD与CQRS组合落地在七猫作家专区项目中使用的层级划分(仅供参考),分为用户接口层、应用层、领域层和基础服务层,代码结构清晰且隔离。(q_XXX中q表示query查询,c_XXX中c表示command命令)

参考文档:

极客时间《DDD 实战课》、《设计模式之美》

Paul Rayner Says DDD and Agile Can Coexist

CQRS

展示评论