从公众号转载,关注微信公众号掌握更多技术动态
---------------------------------------------------------------
一、架构设计简述
1.经典分层图
DDD分层架构的重要原则:每层只能与位于其下方的层发生耦合
User Interface —— 接口/用户界面层。提供与用户/调用者交互的接口,可以是View,也可以是Restful api,还可以是二进制形式的tcp协议接口、自动化测试和批处理脚本等。前端应用通过这一层的接口,向应用层获取展现所需的数据或发送命令给应用层执行用户的命令。数据的组装、数据传输格式以及 Facade 接口等代码都会放在这一层目录里。用户接口层是轻的一层,不含业务逻辑。安全认证,简单的入参校验(例如使用 @Valid 注解),访问日志记录,统一的异常处理逻辑,统一返回值封装应当在这层完成。用户接口层所需要的功能实现是由应用层完成,编码时,该层可以直接引入应用层中定义的接口,因而该层依赖应用层。需要注意的是,虽然理论上用户接口层可以直接使用领域层和基础设施层的能力,但这里建议大家在对这种用法熟练掌握前,最好采用严格的分层架构,即当前层只依赖其下方相邻的一层
Application —— 应用服务层。薄薄的一层,不包含业务逻辑,主要面向用例和流程相关的操作,定义软件要完成的任务,协调多个聚合的服务和领域对象完成服务编排和组合。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。
Domain —— 业务领域对象层。是DDD中的核心层,内聚业务实体的状态和行为,保持领域的一致性,实现企业核心逻辑,保证业务正确性。主要体现模型的业务能力。内部不应该依赖其它层的应用框架(JSP/JSF、Struts、EJB、Hibernate、XMLBeans、Mybatis等)。
包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,实体和领域模型在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务才会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
Infrastructure —— 基础结构层。提供公共服务组件,比如validation、登录态校验、trace日志记录、第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等等。
采用依赖倒置设计,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。例如传统架构中,最担忧的就是换数据库,因为可能要重写大部分代码。采用依赖倒置的设计后,应用层就可以通过解耦来保持独立的核心业务逻辑。数据库变更时,只需要更换数据库基础服务就可以了。
2.领域层交互
(1)修改领域对象
通过仓储获取领域对象,调用领域对象的相关业务方法以完成业务逻辑处理;
(2)新增领域对象
通过构造函数或工厂创建出领域对象,如果需要还可以继续对该新创建的领域对象做一些操作,然后把该新创建的领域对象添加到仓储中;
(3) 删除领域对象
可以先把领域对象从仓储中取出来,然后将其从仓储中删除,也可以直接传递一个要删除的领域对象的唯一标识给仓储通知其移除该唯一标识对应领域对象;
如果一个业务逻辑涉及到多个领域对象,则调用领域层中的相关领域服务完成操作。
二、DDD的详细实现
1. 框架详述
(1)User Interface层
门面层,对外以各种协议提供服务,该层需要明确定义支持的服务协议、契约等。
①dto
数据传输的载体,内部不存在任何业务逻辑,可以通过 DTO 把内部的领域对象与外界隔离。包括request和response两部分,通过它定义入参和出参的契约,在dto层可以使用基础设施层的validation组件完成入参格式校验;
②controller/Facade
支持不同访问协议的控制器实现,比如:http/restful风格、tcp/二进制流协议、mq消息/json对象等等。
controller使用基础设施层公共组件完成许多通用的工作:
-
调用RequestMapping(SpringMVC公共组件)完成servlet路由;
-
调用checklogin完成登录态/权限校验;
-
调用logging组件完成日志记录;
-
调用message-resource组件完成错误信息转义,支持I18N;
③assembler
组装器,负责将多个domain领域对象组装为需要的dto对象,比如查询帖子列表,需要从Post(帖子)领域对象中获取帖子的详情,还需要从User(用户)领域对象中获取用户的基本信息。组装器中不应当有业务逻辑在里面,主要负责格式转换、字段映射等职责。
(2)application层
①service
应用服务层,组合domain层的领域对象和基础设施层的公共组件,根据业务需要包装出多变的服务,以适应多变的业务服务需求。
应用服务层主要访问domain领域对象,调用domain层的方法(不可写业务逻辑)。
应用服务层也会访问基础设施层的公共组件,如rabbitmq,完成领域消息的生产、幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等
②Event(事件)——发布与订阅
这层目录主要存放事件相关的代码。它包括两个子目录:publish 和 subscribe。前者主要存放事件发布相关代码,后者主要存放事件订阅相关代码(事件处理相关的核心业务逻辑在领域层实现)。
③转换器
将DTO转换为领域对象
(3)domain层
业务领域层,是最应当关心的一层,也是最多变的一层,需要保证这一层是高内聚的。确保所有的业务逻辑都留在这一层,而不会遗漏到其他层。
①Aggregate(聚合)
它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。
-
逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。
-
物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。
-
代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。
②domain entity(如果贫血模型,请使用entity )
领域模型和持久模型往往存在阻抗失配,两个模型往往不能达到一致,如果需要的话使用两个类来分别实现。
-
领域模型更倾向于业务场景;
-
领域模型不包含任何框架技术,只有标准库依赖和一些第三方工具类的依赖;
-
领域模型不需要为属性实现set方法,只需要实现业务逻辑的方法和需要属性的get方法,保持最小知识原则;
-
领域模型需要自己实现业务规则的校验方法,比如一台家用轿车有4个轮胎、1个引擎等;
-
领域模型在领域内成熟之后会趋向于稳定。
-
持久模型更倾向于数据库;
-
持久化模型会依赖于实现技术,比如jpa实现就会包含jpa的注解等;
-
持久模型是为ORM或其他持久化技术服务的,一般都需要为每个属性创建get/set方法,是一个贫血模型;
-
持久模型常和一些验证框架一起使用保证数据库数据的合法性@NotNull, @StringLength等等;
-
持久模型随着技术的改进,比如加缓存,分库分表,更换持久化实现,会出现不同形式的更改;
③domain value object领域值对象
④domain service
领域中的一些概念不太适合建模为对象(实体对象或值对象),因为它们本质上就是一些操作、动作,而不是事物(比如:转账服务(transferService),需要操作借方/贷方两个账户实体)。这些操作往往需要 协调多个领域对象。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。DDD认为领域服务模式是一个很自然的范式用来对应这种跨多个对象的操作。一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。
领域服务还有一个很重要的功能就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作,需要了解每个领域对象的业务功能,以及它可能会与哪些其他领域对象交互等一系列领域知识。这样一来,领域层可能会把一部分领域知识泄露到应用层。对于应用层来说,通过调用领域服务提供的简单易懂且意义明确的接口肯定也要比直接操纵领域对象容易的多。
领域行为封装到领域对象中,资源管理行为封装到资源库中,将外部上下文的交互行为封装到防腐层中。领域服务就是通过串联领域对象、资源库和防腐层等一系列领域内的对象的行为,对其他上下文提供交互的接口。
领域服务不能滥用,因为如果我们将太多的领域逻辑放在领域服务上,实体和值对象上将变成贫血对象。
⑤domain event
domain event解决跨aggregate的数据一致性
⑥domain factory(可省略)
DDD中的工厂也是一种体现 封装思想 的模式。DDD中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。工厂是用来封装创建一个复杂对象尤其是聚合时所需的知识,将创建对象的细节(如何实例化对象,然后做哪些初始化操作)隐藏起来。
客户传递给工厂一些简单的参数,如果参数符合业务规则,则工厂可以在内部创建出一个相应的领域对象返回给客户;但是如果参数无效,应该抛出异常,以确保不会创建出一个错误的对象。当然也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,只需要简单的使用构造函数就可以了。隐藏创建对象的好处:可以不让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。
⑦repository(用于访问持久层)
对领域的存储和访问进行统一管理的对象(实体、应用服务都可调用)
仓储被设计出来的原因:领域模型中的对象自从创建后不会一直留在内存活动,当它不活动时会被持久化到DB中,当需要的时候会重建该对象。所以,重建对象是一个和DB打交道的过程,需要提供一种机制,提供类似集合的接口来帮助我们 管理对象。
资源库与DAO多少有些相似之处。但是,资源库和DAO是存在显著区别的。DAO只是对数据库的一层很薄的封装,而资源库则更加具有领域特征。另外,所有的实体都可以有相应的DAO,但并不是所有的实体都有资源库,只有聚合才有相应的资源库。所以说Repository对应的是aggregate而不是entity,虽然我们在实际偏码时不会去专门写aggregate的类,简单地讲根entity就可以代表aggregate。
仓库封装了获取对象的逻辑(例如缓存更新机制等),领域对象无须和底层数据库交互,它只需要从仓库中获取对象即可。仓库可以存储对象的引用,当一个对象被创建后,它可能会被存储到仓库中,那么下次就可以从仓库取。如果用户请求的数据没在仓库中,则会从数据库里取,这就减少了底层交互的次数。当然,仓库获取对象也是有策略的,如下:
注意:
-
仓库的实际实现根据不同的存储介质而不同,可以是redis、oracle、mongodb等。如果各个存储介质的字段属性名不一致,则需要使用translator来做翻译,将持久化层的对象翻译为统一的领域对象。
-
仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中定义仓储的接口,而在基础设施层实现具体的仓储。
⑧translator(领域对象和数据库对象字段相同时省略)
翻译器。将持久化层的对象翻译为统一的领域对象,在仓储层中进行调用。翻译器中不应当有业务逻辑在里面,主要负责格式转换、字段映射等职责。
⑨DTO 转化
正如我们所知,DTO 为系统与外界交互的模型对象,那么肯定会有一个步骤是将 DTO 对象转化为 BO 对象或者是普通的 entity 对象,让 service 层去处理。
(4)infrastructure层
基础设施层提供公共功能组件,供ui、service、domain层调用。
①repository impl
对domain层repository接口的实现,对应每种存储介质有其特定实现,如oracle的mapper,mongodb的dao等等。repository impl会调用mybatis、mongo client、redis client完成实际的存储层操作。
②checkLogin
权限校验器,判定客户端是否有访问该资源的权限。提供给User Interface层的Controller调用。
③exception
异常分类及定义,同时提供公共的异常处理逻辑,具体由ExceptionHandler实现。
④transport
transport完成和第三方服务的交互,可以有多种协议形式的实现,如http+json、tcp+自定义协议等,配套使用的还有Resolver解析器,用于对第三方服务的请求和响应进行适配,提供一个防腐层(AnticorruptionLayer,DDD原书P255)的作用。
⑤logging
日志模块,记录trace日志,使用log4j完成。
⑥util
项目的一些工具类,主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,可以为不同的资源类别建立不同的子目录。
⑦Config:主要存放配置相关代码。
2.软件中的三种服务
(1)服务种类
①应用层服务
用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,负责不同聚合之间的服务、数据协调和调用,负责微服务之间的事件发布和订阅。通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。
应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。
②领域层服务
实现domain的service类。三种service中,唯一可以写业务逻辑的地方。
由于ddd提倡充血模型的缘故,在建模的时候要尽量避免制造domain service。尽量把业务逻辑放到其他的domain object中(比如entity, value object中)。
领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。
③基础层服务/仓储服务
infrastructure service实现不依赖于业务(domain)的功能。简单的例子来讲,比如打印日志(log),发送邮件(如果你的应用软件不是处理邮件问题的话)
infrastructure service位于最底层的infrastructure层。
提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。
(2)微服务内跨层服务调用
微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在 API 网关上的 Facade 服务,Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:
第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。
第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。
二、保证领域模型与代码模型的一致性的方法
DDD 强调先构建领域模型然后设计微服务,以保证领域模型和微服务的一体性,因此不能脱离领域模型来谈微服务的设计和落地。但在构建领域模型时,往往是站在业务视角的,并且有些领域对象还带着业务语言。还需要将领域模型作为微服务设计的输入,对领域对象进行设计和转换,让领域对象与代码对象建立映射关系。
1.从领域模型到微服务的设计
从领域模型到微服务落地,还需要做进一步的设计和分析。事件风暴中提取的领域对象,还需要经过用户故事或领域故事分析,以及微服务设计,才能用于微服务系统开发。这个过程会比事件风暴来的更深入和细致。主要关注内容如下:
-
分析微服务内有哪些服务?
-
服务所在的分层?
-
应用服务由哪些服务组合和编排完成?
-
领域服务包括哪些实体的业务逻辑?
-
采用充血模型的实体有哪些属性和方法?
-
有哪些值对象?
-
哪个实体是聚合根等?
-
最后梳理出所有的领域对象和它们之间的依赖关系,给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。
2.领域层的领域对象
事件风暴结束时,领域模型聚合内一般会有:聚合、实体、命令和领域事件等领域对象。在完成故事分析和微服务设计后,微服务的聚合内一般会有:聚合、聚合根、实体、值对象、领域事件、领域服务和仓储等领域对象。这些领域对象是怎么得来的?
(1)设计实体
大多数情况下,领域模型的业务实体与微服务的数据库实体是一一对应的。但某些领域模型的实体在微服务设计时,可能会被设计为多个数据实体,或者实体的某些属性被设计为值对象。
分析个人客户时,还需要有地址、电话和银行账号等实体,它们被聚合根引用,不容易在领域建模时发现,需要在微服务设计过程中识别和设计出来。
(2)找出聚合根
聚合根来源于领域模型,在个人客户聚合里,个人客户这个实体是聚合根,它负责管理地址、电话以及银行账号的生命周期。个人客户聚合根通过工厂和仓储模式,实现聚合内地址、银行账号等实体和值对象数据的初始化和持久化。
聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。
(3) 设计值对象
根据需要将某些实体的某些属性或属性集设计为值对象。值对象类放在代码模型的 Entity 目录结构下。在个人客户聚合中,客户拥有客户证件类型,它是以枚举值的形式存在,所以将它设计为值对象。
有些领域对象可以设计为值对象,也可以设计为实体,需要根据具体情况来分析。如果这个领域对象在其它聚合内维护生命周期,且在它依附的实体对象中只允许整体替换,就可以将它设计为值对象。如果这个对象是多条且需要基于它做查询统计,建议将它设计为实体。
(4)设计领域事件
如果领域模型中领域事件会触发下一步的业务操作,就需要设计领域事件。首先确定领域事件发生在微服务内还是微服务之间。然后设计事件实体对象,事件的发布和订阅机制,以及事件的处理机制。判断是否需要引入事件总线或消息中间件。
在个人客户聚合中有客户已创建的领域事件,因此它有客户创建事件这个实体。
(5)设计领域服务
如果一个业务动作或行为跨多个实体,就需要设计领域服务。领域服务通过对多个实体和实体方法进行组合,完成核心业务逻辑。可以认为领域服务是位于实体方法之上和应用服务之下的一层业务逻辑。
按照严格分层架构层的依赖关系,如果实体的方法需要暴露给应用层,它需要封装成领域服务后才可以被应用服务调用。所以如果有的实体方法需要被前端应用调用,会将它封装成领域服务,然后再封装为应用服务。
个人客户聚合根这个实体创建个人客户信息的方法,被封装为创建个人客户信息领域服务。然后再被封装为创建个人客户信息应用服务,向前端应用暴露。
(6)设计仓储
每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。仓储包括仓储的接口和仓储实现,通过依赖倒置实现应用业务逻辑与数据库资源逻辑的解耦。
3.应用层的领域对象
在事件风暴或领域故事分析时,往往会根据用户或系统发起的命令,来设计服务或实体方法。为了响应这个命令,需要分析和记录:
-
在应用层和领域层分别会发生哪些业务行为;
-
各层分别需要设计哪些服务或者方法;
-
这些方法和服务的分层以及领域类型(比如实体方法、领域服务和应用服务等),它们之间的调用和组合的依赖关系。
在严格分层架构模式下,不允许服务的跨层调用,每个服务只能调用它的下一层服务。服务从下到上依次为:实体方法、领域服务和应用服务。如果需要实现服务的跨层调用,应该怎么办?建议采用服务逐层封装的方式。
服务的封装和调用主要有以下几种方式。
(1)实体方法的封装
实体方法是最底层的原子业务逻辑。如果单一实体的方法需要被跨层调用,可以将它封装成领域服务。如果它还需要被用户接口层调用,还需要将这个领域服务封装成应用服务。经过逐层服务封装,实体方法就可以暴露给上面不同的层,实现跨层调用。
封装时服务前面的名字可以保持一致,可以用 *DomainService 或 *AppService 后缀来区分领域服务或应用服务。
(2)领域服务的组合和封装
领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。
(3)应用服务的组合和编排
应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。
在应用服务组合和编排时,需要关注一个现象:多个应用服务可能会对多个同样的领域服务重复进行同样业务逻辑的组合和编排。当出现这种情况时,就需要分析是不是领域服务可以整合了。可以将这几个不断重复组合的领域服务,合并到一个领域服务中实现。这样既省去了应用服务的反复编排,也实现了服务的演进。这样领域模型将会越来越精炼,更能适应业务的要求。
4.领域对象与微服务代码对象的映射
(1)典型的领域模型
个人客户领域模型中的个人客户聚合,就是典型的领域模型,从聚合内可以提取出多个实体和值对象以及它的聚合根。
看一下下面这个图,对个人客户聚合做了进一步的分析。提取了个人客户表单这个聚合根,形成了客户类型值对象,以及电话、地址、银行账号等实体,为实体方法和服务做了封装和分层,建立了领域对象的关联和依赖关系,还有仓储等设计。关键是这个过程,建立了领域对象与微服务代码对象的映射关系。
(2)非典型领域模型
有些业务场景可能并不能如你所愿,可能无法设计出典型的领域模型。这类业务中有多个实体,实体之间相互独立,是松耦合的关系,这些实体主要参与分析或者计算,找不出聚合根,但就业务本身来说它们是高内聚的。而它们所组合的业务与其它聚合是在一个限界上下文内,也不大可能将它单独设计为一个微服务。
这种业务场景其实很常见。比如,在个人客户领域模型内有客户归并的聚合,它扫描所有客户,按照身份证号码、电话号码等是否重复的业务规则,判断是否是重复的客户,然后对重复的客户进行归并。这种业务场景就找不到聚合根。
那对于这类非典型模型,怎么办?
还是可以借鉴聚合的思想,仍然用聚合来定义这部分功能,并采用与典型领域模型同样的分析方法,建立实体的属性和方法,对方法和服务进行封装和分层设计,设计仓储,建立领域对象之间的依赖关系。唯一可惜的就是依然找不到聚合根,不过也没关系,除了聚合根管理功能外,还可以用 DDD 的其它设计方法。
三、具体实现
1.接口层实现
User Interface层是用户接口层,为用户/调用方提供可访问的接口,我们简称为“UI”层,在UI层中,我们还会去使用infrastructure层中的validation、logging、checkLogin等公共组件完成一些通用的动作。
(1) Controller(对象的传递使用dto)
这里的controller承担这一个请求受理的角色.
-
接受请求;
-
请求格式校验及转换;
-
权限校验;
-
路由请求;
-
记录请求;
-
回复响应;
//开发中的权限拦截,异常处理,日志等通过全局进行处理,也就是aop
@Controller
@RequestMapping("/product")
public class ProductController{
@Autowired
private ProductService productService;
@Resource(name="productDTOConvert")
private DTOConvert<ProductDTO,Product> dTOConvert;
//添加商品
@ResponseBody
@RequestMapping(value = "/add", method = RequestMethod.POST)
public String posting(ProductDTO productDTO) throws Exception {
Product product = dTOConvert.convert(productDTO);
String result = productService.addProduct(product);
return result;
}
//查询商品详情
@ResponseBody
@RequestMapping(value = "/select", method = RequestMethod.POST)
public ProductDTO queryPostDeatil(int id) {
ProductDTO productDTO = productService.findProductDetail(id);
return productDTO;
}
}
(2)DTO
public class ProductDTO { private String id; private String name; //被哪个商户添加的 private String addByUser; private BigDecimal price; }
2.infrastructure层的公共组件
(1)一些资源过滤器,拦截器
checkLogin、Logging、Validation、ExceptionHandler
(2)仓储实现类
//用于做仓库存储,该仓促内部可调用缓存或者持久化等操作。
@Repository
public class ProductRespository implements IProductRepository {
@Autowired
private ProductMapper productMapper;
public Product query(int productId) {
Product product = productMapper.query(productId);
return product;
}
public int save(Product product) {
int count = productMapper.save(product);
return count;
}
public int delete(int productId) {
int count = productMapper.delete(productId);
return count;
}
}
(3)持久化包、缓存等数据层操作
public interface ProductMapper {
//查询指定商品
Product query(int productId);
//保存商品
int save(Product product);
//删除商品
int delete(int productId);
}
省略ProductMapper.xml文件
3. Application层
(1) Service组件粘合剂
通过不断的实践,我们发现:通过DDD实现业务服务时,检验业务模型的质量的一个标准便是 —— service方法中不要有if/else。如果存在if/else,要么就是系统用例存在耦合,要么就是业务模型不够友好,导致部分业务逻辑泄漏到service了。
通常意义上,一个业务case在service层便会对应一个service方法,这样确保case实现的独立性。拿商城中的“商品”模块来讲,我们有如下几个明显的case:发布商品、删除商品、查询商品详情,这些case在service层都对应独立的业务方法。
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private IProductRepository productRepository;
@Autowired
private ProductAssembler productAssembler;
public String deleteProduct(int id) {
// TODO Auto-generated method stub
return null;
}
public String addProduct(Product product) {
//此时参数已经在controller通过公共组件校验完毕
int count = productRepository.save(product);
if(count > 0){
return "success";
}
return "error";
}
public ProductDTO findProductDetail(int id) {
Product product = productRepository.query(id);
return productAssembler.assembleFindProductDetailResp(product);
}
}
(2)assembler组装器
负责完成domain model对象到dto的转换,组装职责包括:
-
完成类型转换、数据格式化;如日志格式化,状态enum装换为前端认识的string;
-
将多个domain领域对象组装为需要的dto对象,比如查询帖子列表,需要从Post(帖子)领域对象中获取帖子的详情,还需要从User(用户)领域对象中获取用户的社交信息(昵称、简介、头像等);
-
将domain领域对象属性裁剪并组装为dto;某些场景下,可能并不需要所有domain领域对象的属性,比如User领域对象的password属性属于隐私相关属性,在“查询用户信息”case中不需要返回,需要裁剪掉。
/*
* 组装器,完成domain model对象到dto的转换,组装职责包括:
* 1、完成类型转换、数据格式化;如日志格式化,状态enum装换为前端认识的string;
* 2、将多个model组合成一个dto,一并返回。
* */
@Component
public class ProductAssembler {
public ProductDTO assembleFindProductDetailResp(Product product) {
ProductDTO productDTO = new ProductDTO();
//此处省略相关封装
return productDTO;
}
}
注意:如果只是普通对象的组装,可以抽象成一个带泛型的接口,然后让各个实现类实现接口。
(3)转换器
public interface DTOConvert<S,T> {
T convert(S s)throws Exception;
}
@Component
public class ProductDTOConvert implements DTOConvert<ProductDTO,Product> {
@Override
public Product convert(ProductDTO productDTO) throws InvocationTargetException, IllegalAccessException {
Product goods = new Product();
BeanUtils.copyProperties(productDTO,goods);
return goods;
}
}
4.Domain层
(1)domain entity
在商城这一业务领域中,‘商品’就是一个业务实体,它需要有一个唯一性业务标识表征,拥有这个业务实体相关的业务属性(商品名、发布者、等)和业务行为(添加商品、删除商品等),同时他的状态和内容可以不断发生变化。
//商品;类
public class Product {
private String id;
private String name;
//被哪个商户添加的
private String addByUser;
private BigDecimal price;
//省略getter,setter
public void addProduct(){
//进行相关业务逻辑,比如判断价钱,用户角色等
}
}
(2) repository
注:可以抽离一个共有的泛型仓储接口,内部定义一些共有的方法,各自的仓储接口实现它。
将仓库的接口定义归类在domain层,因为他和domain entity联系紧密。仓库接口定义了和基础实施的持久化层交互契约,完成领域对应的增删改查操作。domain层的repository只是定义契约的接口,实际实现仍然由infrastructure完成。
public interface IProductRepository {
//查询指定商品
Product query(int productId);
//保存商品
int save(Product product);
//删除商品
int delete(int productId);
}
系统架构13
系统架构 · 目录
上一篇网站可扩展架构设计——领域驱动设计(中)