领域驱动设计实践:以DDD视角看SOFA

一、前言

最近在蚂蚁接触了一段时间的SOFA,决定写一篇文章记录下学习心得。DDD是2004年被提出的,那时的程序还仅仅是部署单机。到了2014年,SOA大行其道,微服务的概念开始冒头,如何将一个应用合理的拆分为多个微服务成为了各大论坛的热门话题,而DDD里面的Bounded Context(限界上下文)的思想为微服务拆分提供了一套合理的框架。

二、概念

1. 领域+驱动+设计

在《领域驱动设计》一书中,作者 Eric Evans 讲过:在项目的最初阶段,需要业务,产品和技术同时参与进来。尽可能用建模语言把业务含义和产品功能描述清楚。

领域驱动设计不是一种固定的设计模式,而是一种思考问题的方法论,强调对实际问题建模,它以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,然后将这些概念设计成一个领域模型。从而利用领域模型驱动软件设计,用代码来实现该领域模型。所以,DDD 的核心是建立正确的领域模型

在这里插入图片描述

领域驱动设计 = “领域” + “驱动” + “设计”。

这里的“领域”实际上指的是用一套系统化的、技巧的方法来分析领域的复杂度,让开发人员能够更好地学习业务知识,识别业务复杂度(领域实体关系),控制业务复杂度;让领域专家、需求分析人员,开发技术人员都能够统一理解,使设计能够更好地表达和支撑业务。

驱动指的是通过领域分析的方法,通过建模来驱动、指导实际的开发设计。

设计指的是最终能够落地成代码的设计,设计出来的领域模型绑定了领域和代码,使代码实现能够解决领域中的问题,实现需求。

2. 何为领域

为了创建一个好软件,你必须知道这个软件究竟是什么。在你充分 了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域。我们怎样才能让软件和领域和谐相处呢?最佳的方式是让软件成为领域的反射(映射)。软件需要具现领域里重要的核心概念和元素,并精确实现它们之间的关系。软件需要对领域进行建模。

例如我要开发一个银行软件系统,我就肯定需要召集理解银行领域的领域专家。

在这里插入图片描述

因此DDD提倡在软件设计初期召集开发人员和领域专家,一起建立领域模型,让开发人员能够更好地理解业务。而实际开发过程中,涉及的项目可能不会特别复杂,此时领域专家通常就是开发人员自己。

3. 领域划分

对领域进行划分并不是一个一蹴而就的过程,应该是个动态调整的过程,通常分为以下几步:

  • 把整个系统的业务子模块梳理出来

  • 把业务关联性比较强的业务划分到一起,形成各个子域

  • 不断地做微调,形成一个最终的领域划分

例如下图对于客服系统的开发,先分析客服系统的关联业务,再按照业务相关性划分为不同的子域。

在这里插入图片描述

三、DDD结构

在这里插入图片描述

与传统的三层架构相比,领域驱动下的分层架构有所变化。首先,原先的业务层(逻辑层)被细分为应用层和领域层(biz层和core层)。原先的数据访问被合并到基础结构层(common层)。表现层(web层)保持不变。

1. 基础结构层

首先,它为上层提供技术框架的支持,比如各种Utils。其次,数据访问也被整合到该层中,因为数据的读写应该是和具体业务场景无关的。

2. 领域层

领域层包含了领域对象(实体、值对象)、对象关系处理逻辑、领域服务。该层维护了该领域中,各个实体不随业务场景变化的关联关系,以服务的方式提供给应用层调用。

3. 应用层

该层实现各个业务场景的具体用例,能够给不同的用户需求提供不同的产品。这一层只负责具体业务场景的流程维护,不包含领域逻辑。

4. 表现层

和传统分层中的表现层没有区别。

四、SOFA结构

1. 分层架构

在这里插入图片描述

+-- appname
  +-- app               (应用目录)
    |-- test             (测试层)
    +-- web             (视图展现层)
        |--home          (web-home 是 Web 层中的公用 Web 模块)
        |--web-prodn     (web-prod1、web-prod2 等是可选的 Web 模块) 
    +-- biz             (业务应用层,当业务逻辑没有复杂到使用核心领域层时,Biz 层相当于传统分层架构中的业务逻辑)
        |--shared        (Biz 层的模块划分与 web 对应,公用的应用逻辑封装在 biz-shared 模块中,与 web-home 对应)
        |--prodn         (与web-prod1对应)
        |--service-impl  (封装了对外发布的服务接口的具体实现) 
    +-- core             (核心领域曾)
        |--service       (模块封装核心业务,提供核心领域服务)
        |--model         (包含领域层各个模型对象)    
    +-- common           (基础结构层)
        +--service       
            |--facade     (对外发布的服务接口)
            |--integration(第三服务调用接口)
        |--util          (基础的公用的工具服务)
        |--dal           (相当于传统分层中的数据访问层)
  |-- assembly         (装配目录 - 可选)
  |-- conf             (SOFA 相关配置存放目录)
  |-- webroot           (静态资源和 MVC 模块文件存放路径)
  |-- pom.xml          (总POM文件)

1.1 common 层(基础结构层)

在 common 层中包含了为系统提供基础服务的各个模块:

util 模块提供了基础的公用的工具服务;common-dal 模块相当于传统分层中的数据访问层,封装了对数据库的访问逻辑,向上暴露 DAO 服务。

common-dal 模块位于依赖链的最底层,所有模块都会直接或间接的依赖它,使用其 DAO 服务,但由于暴露的 DAO 服务很多、粒度很细,如果全部发布为模块化服务会增加额外的工作量,所以在实际实践中其他模块一般通过 Spring-Parent 的方式来依赖 common-dal ,从而直接使用 DAL 层的 DAO 服务。

common-service-facade 是提供给别的应用使用服务的 API 层,一般只有接口定义;common-service-integration 是引用别的应用发布的服务供本应用使用。

1.2 core 层(核心领域层)

在传统的分层设计中是不包含领域层的,而是直接在 DAL 层之上设计 Biz 业务逻辑层。而当业务发展到一定深度和成熟度、甚至可以制定行业标准时,就有必要进行领域层的设计。当然,按照领域驱动设计的方式,在一开始就进行领域层的设计,为以后的业务扩展提供支持,也是一种良好的设计方案。

core 层分为两个模块,core-model 模块包含领域层各个模型对象;core-service 模块封装核心业务,提供核心领域服务。core-service 模块被 biz 层依赖,为其提供核心领域服务,又依赖同层的 core-model ,使用其定义的模型对象,起到承上启下的作用。

命名规范:领域服务的命名以 Service 结尾。

1.3 biz 层(业务应用层)

一般情况下,当业务逻辑没有复杂到使用核心领域层时,biz 层相当于传统分层架构中的业务逻辑层:可以直接设计在 DAL 层之上,调用 DAL 提供的数据访问服务,使用 DAL 层的 DO 作为数据传输对象,在此层实现所有业务逻辑,向展现层暴露业务服务接口。当引入了领域层时,biz 层相当于领域驱动设计中的应用层:位于领域层之上,调用的是领域服务,使用的是领域模型,自己则专注于具体应用所需要的逻辑处理,而不包含核心业务规则,更多的是给领域层需要协作的各个领域服务协调任务、委派工作。

biz 层的模块划分与 web 对应,公用的应用逻辑封装在 biz-shared 模块中,与 web-home 对应(见下)。biz 层可以根据实际业务需求创建新的模块,处理不同类型的业务场景,它们之间是同级的,不应该存在互相依赖,在开发中不要手动创建 biz 层各模块间的依赖,这样不但会造成业务逻辑的混乱,也可能会形成循环依赖,所有的公用逻辑应该放到 biz-shared 中;其他 biz 模块都依赖 biz-shared,但又不互相依赖。

biz-service-impl 模块中封装了对外发布的服务接口的具体实现。提供给外部系统调用的服务分为两部分,接口定义放在 common 层的 common-service-facade 模块,外部系统只需定义对该 facade 模块的依赖便可以 stub 的形式使用接口定义;而接口的实现则放在 biz 层的 biz-service-impl 模块。该模块的业务可能会引用 biz-shared 和其他 biz 模块中的服务,所以它可以依赖 biz 层的所有模块,但自动生成的 SOFA 工程只配置了 biz-service-impl 对 biz-shared 和 common-service-facade 的依赖,对其它 biz 模块的依赖需要手根据实际需要动添加。所有 biz 层模块都依赖于 biz-shared ,且通过它间接依赖 core 层和 common 层。

命名规范:该层业务服务类以 Manager 结尾,包装的门面以 Facade 结尾。

1.4 web 层(展现层)

web 层是应用的视图展现层,SOFA MVC 专门为其提供了强大的 MVC 框架,该层是可选的,如果只开发纯业务的核心系统,可以去掉这一层,需要结合 web 层的项目可以查阅 SOFA MVC 的相关文档。

web-home 是 web 层中的公用 web 模块,它包含了运行视图层需要的所有公共组件的配置,是 SOFA MVC 能正常运行的基础。该模块中可以放一些全局的处理逻辑,比如首页访问请求、全局错误处理等。web 层中可以根据实际应用的需要创建新的模块,用于处理不同类型的页面请求,它们之间是同级的,不存在互相依赖,在开发中也不要手动建立 web 层各模块间的依赖,这样不但会造成业务逻辑的混乱,也可能会形成循环依赖,所有公共的页面处理逻辑都应该放到 web-home 里。所有 web 模块都依赖 web-home ,且通过它间接依赖 biz、core 和 common 层。

1.5 test 层(测试层)

该层是 SOFA 项目中测试模块,提供了单元测试的基类,供开发人员继承或扩展。由于要对所有模块进行测试,因此该层位于 SOFA 系统最顶端,可谓高瞻远瞩俯视群雄,它通过直接和间接依赖,可以访问到每个模块的代码,也即所有模块对测试层都是可见的。

1.6 分层作用

通过分层,SOFA可以将通用能力下沉,将具体的业务逻辑放在核心领域层,同时也避免了业务层对基础层的直接调用,将业务层与基础层实现了一定意义上的解耦。

在这里插入图片描述

2. 领域层

领域层是SOFA的核心,接下来我以一个例子介绍领域层所做的事情。

这是一个未经过重构的项目结构,UI层依赖业务层,业务层依赖基础设施层,业务与数据直接耦合。

在这里插入图片描述

2.1 抽象数据存储层

领域层做的第一个工作就是抽象数据存储层,这里首先要介绍几个概念。

  • Data Object数据类:DO是单纯的和数据库表的映射关系,每个字段对应数据库表的一个column,只有数据,没有行为。

  • Entity实体类: Entity是基于领域逻辑的,它的字段和数据库储存不需要有必然的联系。Entity包含数据,同时也应该包含行为。实体对象是我们正常业务应该用的业务模型。Entity的生命周期应该仅存在于内存中,不需要可序列化和可持久化。

  • Repository仓储层:对应Entity对象读取储存的抽象,在接口层面做统一,不关注底层实现。比如,通过 save 保存一个Entity对象,但至于具体是 insert 还是 update 并不关心。Repository的具体实现类通过调用DAO来实现各种操作,通过Builder/Factory对象实现AccountDO 到 Account之间的转化

以往的CRUD是面向数据库的,DO对象仅仅是对数据表的映射,这样能带来一个好处即开发人员能基于数据库层面思考业务。然而DO对象仅仅是数据库的映射,并不具有业务意义,此时可以通过实体类将DO对象进行抽象。创建实体类Entity,封装Do对象,此时的Entity不仅包含数据,同时可以包含行为。这里Entity我理解为领域模型。

在这里插入图片描述
在这里插入图片描述

2.2 封装业务逻辑

在领域层可以对业务逻辑进行封装,这里主要总结两种封装,即Entity封装业务行为和领域服务封装业务逻辑

在这里插入图片描述

(1)Entity封装业务行为

前面也提到了,Entity不仅包含数据,同时可以包含行为。例如Account,不仅可以有用户信息,同时可以将转账的行为封装进Account实体类,给模型服务业务意义。

在这里插入图片描述

代码示例

在这里插入图片描述

(2)领域服务封装业务逻辑

对于多实体对象逻辑,创建新类,封装为领域服务。例如两个Account之间的转账,可以单独抽象作为转账服务。在领域层抽象通用服务,方便复用。

在这里插入图片描述

代码示例

在这里插入图片描述

重构后
在这里插入图片描述

2.3 总结

通过前两步的抽象,形成了所谓的领域模型与领域服务。

在这里插入图片描述

3. 业务层

业务层我想聊聊model到dto的转换,从领域层接收到的数据为model,然而我希望传给应用层的是dto。

为何不能直接将领域对象用于数据传递?

  • 领域对象更注重领域,而DTO更注重数据。传输对象DTO本身并不是业务对象。数据传输对象是根据UI的需求进行设计的,而不是根据领域对象进行设计的。

  • 避免直接将领域对象的行为暴露给表现层。同时对外的DTO不应感知领域的变化,无论内部如何变化,对外都不能直接耦合,应该经过convertor转化后保持一致。

在这里插入图片描述

4. Common层

在这里插入图片描述

Common层主要想聊聊Integration。SOFA中将集成外部服务放在了Integration,将内部接口放在facade并对外暴露,这样的结构十分清晰。

其中Integration让我想到了DDD中提到的ACL防腐层的概念。

ACL防腐层

在这里插入图片描述

  • 于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类

  • 构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类

  • 针对外部系统调用,同样的用Facade方法封装外部调用链路

上述提到的Facade类指的是Facade设计模式,将多个接口封装形成自己的接口,而不是SOFA中的facade。

通过ACL防腐层,就算服务依赖于上游服务,当上游服务发生变化时也不会直接影响当前服务。

五、DDD与MVC

这两个概念经常混淆,其实这两种模式,是不可与之比较的,因为MVC是技术角度设计技术架构,DDD则是从业务的角度设计业务架构,两者是相辅相成的模式。

DDD与传统业务划分模式的区别,往常我们划分业务,大多是根据数据库实体来划分一个service,然后所有相关这个实体的操作都写在这个service里面,这样的设计在业务简单时是完全OK的,但是,在后面的业务发展中,service往往会臃肿不堪,举个例子,比如一个订单service,我们一开始会做订单提交之类简单的逻辑,后面就会新增订单统计相关的逻辑,这些逻辑看似与订单有关,但又可以独立出去,我们没有一个准则来划分;而DDD就为我们提供了这样一套准则,帮我们去更好的划分业务领域,去分类;也就是说DDD给我们提供了业务划分的准则,与传统模式相比,更加具有指导性,有规范。

六、总结

以上就是我以DDD视角对SOFA做的简单分析,通过领域层的设计给模型赋予业务意义,将通用能力下沉,对于复杂项目十分有必要。(虽然大部分项目其实并没那么复杂_

我是Apollo,如果对我的文章感兴趣欢迎关注我!

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值