目录
概念
领域+驱动+设计 DOMAIN DRIVEN DESIGN
在《领域驱动设计》一书中,作者 Eric Evans 讲过:在项目的最初阶段,需要业务,产品和技术同时参与进来。尽可能用建模语言把业务含义和产品功能描述清楚。
领域驱动设计不是一种固定的设计模式,而是一种思考问题的方法论,强调对实际问题建模,它以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,然后将这些概念设计成一个领域模型。从而利用领域模型驱动软件设计,用代码来实现该领域模型。所以,DDD 的核心是建立正确的领域模型。
领域驱动设计 = “领域” + “驱动” + “设计”。
这里的“领域”实际上指的是用一套系统化的、技巧的方法来分析领域的复杂度,让开发人员能够更好地学习业务知识,识别业务复杂度(领域实体关系),控制业务复杂度;让领域专家、需求分析人员,开发技术人员都能够统一理解,使设计能够更好地表达和支撑业务。
驱动指的是通过领域分析的方法,通过建模来驱动、指导实际的开发设计。
设计指的是最终能够落地成代码的设计,设计出来的领域模型绑定了领域和代码,使代码实现能够解决领域中的问题,实现需求。
何为领域 DOMAIN DRIVEN DESIGN
为了创建一个好软件,你必须知道这个软件究竟是什么。在你充分了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域。我们怎样才能让软件和领域和谐相处呢?
最佳的方式是让软件成为领域的反射(映射)。软件需要具现领域里重要的核心概念和元素,并精确实现它们之间的关系。软件需要对领域进行建模。
例如:我要开发一个银行软件系统,我就肯定需要召集理解银行领域的领域专家。
因此DDD提倡在软件设计初期召集开发人员和领域专家,一起建立领域模型,让开发人员能够更好地理解业务。而实际开发过程中,涉及的项目可能不会特别复杂,此时领域专家通常就是开发人员自己。
领域划分 DOMAIN DRIVEN DESIGN
对领域进行划分并不是一个一蹴而就的过程,应该是个动态调整的过程,通常分为以下几步:
-
把整个系统的业务子模块梳理出来
-
把业务关联性比较强的业务划分到一起,形成各个子域
-
不断地做微调,形成一个最终的领域划分
例如,下图对于客服系统的开发,先分析客服系统的关联业务,再按照业务相关性划分为不同的子域。
DDD结构
与传统的三层架构相比,领域驱动下的分层架构有所变化。首先,原先的业务层(逻辑层)被细分为应用层和领域层(biz层和core层)。原先的数据访问被合并到基础结构层(common层)。表现层(web层)保持不变。
1. 基础结构层规约
首先,它为上层提供技术框架的支持,比如各种Utils。其次,数据访问也被整合到该层中,因为数据的读写应该是和具体业务场景无关的。
2. 领域层
领域层包含了领域对象(实体、值对象)、对象关系处理逻辑、领域服务。该层维护了该领域中,各个实体不随业务场景变化的关联关系,以服务的方式提供给应用层调用。
3. 应用层
该层实现各个业务场景的具体用例,能够给不同的用户需求提供不同的产品。这一层只负责具体业务场景的流程维护,不包含领域逻辑。
4. 表现层
和传统分层中的表现层没有区别。
SOFA结构
分层架构 DOMAIN DRIVEN DESIGN
+-- 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. common 层(基础结构层)
在 common 层中包含了为系统提供基础服务的各个模块:
util 模块提供了基础的公用的工具服务;common-dal 模块相当于传统分层中的数据访问层,封装了对数据库的访问逻辑,向上暴露 DAO 服务。
common-dal 模块位于依赖链的最底层,所有模块都会直接或间接的依赖它,使用其 DAO 服务,但由于暴露的 DAO 服务很多、粒度很细,如果全部发布为模块化服务会增加额外的工作量,所以在实际实践中其他模块一般通过 Spring-Parent 的方式来依赖 common-dal ,从而直接使用 DAL 层的 DAO 服务。
common-service-facade 是提供给别的应用使用服务的 API 层,一般只有接口定义;common-service-integration 是引用别的应用发布的服务供本应用使用。
2. core 层(核心领域层)
在传统的分层设计中是不包含领域层的,而是直接在 DAL 层之上设计 Biz 业务逻辑层。而当业务发展到一定深度和成熟度、甚至可以制定行业标准时,就有必要进行领域层的设计。当然,按照领域驱动设计的方式,在一开始就进行领域层的设计,为以后的业务扩展提供支持,也是一种良好的设计方案。
core 层分为两个模块,core-model 模块包含领域层各个模型对象;core-service 模块封装核心业务,提供核心领域服务。core-service 模块被 biz 层依赖,为其提供核心领域服务,又依赖同层的 core-model ,使用其定义的模型对象,起到承上启下的作用。
命名规范:领域服务的命名以 Service 结尾。
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 结尾。
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 层。
5. test 层(测试层)
该层是 SOFA 项目中测试模块,提供了单元测试的基类,供开发人员继承或扩展。由于要对所有模块进行测试,因此该层位于 SOFA 系统最顶端,可谓高瞻远瞩俯视群雄,它通过直接和间接依赖,可以访问到每个模块的代码,也即所有模块对测试层都是可见的。
6. 分层作用
通过分层,SOFA可以将通用能力下沉,将具体的业务逻辑放在核心领域层,同时也避免了业务层对基础层的直接调用,将业务层与基础层实现了一定意义上的解耦。
领域层 DOMAIN DRIVEN DESIGN
领域层是SOFA的核心,接下来我以一个例子介绍领域层所做的事情。
这是一个未经过重构的项目结构,UI层依赖业务层,业务层依赖基础设施层,业务与数据直接耦合。
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. 封装业务逻辑
在领域层可以对业务逻辑进行封装,这里主要总结两种封装,即Entity封装业务行为和领域服务封装业务逻辑
(1)Entity封装业务行为
前面也提到了,Entity不仅包含数据,同时可以包含行为。例如Account,不仅可以有用户信息,同时可以将转账的行为封装进Account实体类,给模型服务业务意义。
代码示例
(2)领域服务封装业务逻辑
对于多实体对象逻辑,创建新类,封装为领域服务。例如两个Account之间的转账,可以单独抽象作为转账服务。在领域层抽象通用服务,方便复用。
代码示例
重构后
3. 总结
通过前两步的抽象,形成了所谓的领域模型与领域服务。
业务层 DOMAIN DRIVEN DESIGN
业务层我想聊聊model到dto的转换,从领域层接收到的数据为model,然而我希望传给应用层的是dto。
为何不能直接将领域对象用于数据传递?
-
领域对象更注重领域,而DTO更注重数据。传输对象DTO本身并不是业务对象。数据传输对象是根据UI的需求进行设计的,而不是根据领域对象进行设计的。
-
避免直接将领域对象的行为暴露给表现层。同时对外的DTO不应感知领域的变化,无论内部如何变化,对外都不能直接耦合,应该经过convertor转化后保持一致。
Common层 DOMAIN DRIVEN DESIGN
Common层主要想聊聊Integration。SOFA中将集成外部服务放在了Integration,将内部接口放在facade并对外暴露,这样的结构十分清晰。
其中Integration让我想到了DDD中提到的ACL防腐层的概念。
ACL防腐层 DOMAIN DRIVEN DESIGN
-
于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类
-
构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类
-
针对外部系统调用,同样的用Facade方法封装外部调用链路
上述提到的Facade类指的是Facade设计模式,将多个接口封装形成自己的接口,而不是SOFA中的facade。
通过ACL防腐层,就算服务依赖于上游服务,当上游服务发生变化时也不会直接影响当前服务。
DDD与MVC
这两个概念经常混淆,其实它们没有比较的意义,因为MVC是技术角度设计技术架构,DDD则是从业务的角度设计业务架构,两者是相辅相成的模式。
DDD与传统业务划分模式的区别,往常我们划分业务,大多是根据数据库实体来划分一个service,然后所有相关这个实体的操作都写在这个service里面,这样的设计在业务简单时是完全OK的。
但是,在后面的业务发展中,service往往会臃肿不堪,举个例子,比如一个订单service,我们一开始会做订单提交之类简单的逻辑,后面就会新增订单统计相关的逻辑,这些逻辑看似与订单有关,但又可以独立出去,我们没有一个准则来划分。而DDD就为我们提供了这样一套准则,帮我们去更好地划分业务领域,去分类。
也就是说DDD给我们提供了业务划分的准则,与传统模式相比,更加具有指导性,有规范。
总结
以上就是我以DDD视角对SOFA做的简单分析,通过领域层的设计给模型赋予业务意义,将通用能力下沉,对于复杂项目十分有必要。