领域驱动设计工程规范(参考)

  |   0 评论   |   0 浏览

背景

X项目组后端工程都是根据DDD的规范搭建的工程,整体结构上是统一的,但是在实现细节上,每个人习惯不同,代码风格有很大差异,导致工程之间的风格迥异,维护成本变高,因此需要梳理一套组内通用的项目工程规范,通过规范让编码风格统一

目标

建立一套组内通用的工程规范,组内成员达成一致,严格按照规范执行,以达到提供代码可读性、工程可维护性的目的。同时提高协作沟通效率。

调研

领域驱动设计调研

结论:需要根据自己的业务需求,建立适合业务场景的架构模式。没有统一的答案,每个团队在DDD实践上都有自己的理解,并没有统一的标准

1.规范组成

主要有两大部分组成:

  • 开发规范
  • 运维规范

名词解释

规范约束词:

强制: 必须遵守

推荐: 建议的做法,通常情况下是最合理的

参考: 提供一种综合考虑较优的方案

2.DDD工程实践概述

领域驱动-补充资料-分层架构的背景

分为四层:

(1)用户界面层(或表示层)

(2)应用层

(3)领域层

(4)基础设施层

各层的关联方式:

  • 各层之间是松散连接的
  • 层与层之间的依赖关系只能是单向的
  • 上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段
  • 而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用 另一种通信机制,使用架构模式来连接上下层,如回调模式或OBSERVERS模式

一个下单的场景,下单完成需要发送邮件通知,发送异步mq

由于基础设施层的南向网关与北向网关扮演的角色并不相同,它们所服务的调用者存在明显的差别。南向网关中的资源库实现会与数据库交互,主要的调用者为领域服务或应用服务,故而需要提供持久化操作的数据对象。北向网关则服务于前端或外部调用者,属于服务模型驱动设计中定义的远程服务对象。

领域驱动的整体架构:

  • 通过Repository Interface将数据表内容转化为Entity
  • Service封装了与Entity、Value object 无关的业务逻辑
  • 领域层关注行为,Interface代表一种行为,是稳定的,实现放在基础设施层,实现可以替换

3.工程结构概览

工程结构:

详细解释见:DDD工程结构详细说明

4. 工程规约


DDD分层领域模型规约:

  • DTO(Data Transfer Object ):Thrift/pigeon 接口定义对象命名方式,命名方式XxxDTO
  • DO(Data Object):持久层对象命名方式。命名方式XxxDO
  • Entity(实体):代表一个领域实体。命名方式XxxEntity
  • ValueObject(值对象):代表一个领域值对象。命名方式XxxValue

4.1.应用层

应用服务是领域模型的直接客户,应用层要尽量简单,不包含业务规则或者知识,而只为下一层(指领域层)中的领域对象协调任务,分配工作,使它们互相协作。

领域层提供了细粒度的领域模型对象,不利于它的客户端调用。因此,“基于 KISS(Keep It Simple and Stupid)原则或最小知识原则,我们希望调用者了解的知识越少越好,调用变得越简单越好,这就需要引入一个间接的层来封装。这就是应用层存在的主要意义

1.【强制】禁止在应用层写任何领域逻辑,仅负责协调领域模型对象,通过它们的领域能力来组合完成一个完整的应用目标

2.【强制】与横切关注点相关的内容应该放在应用层

举例:应用层层参数校验建议放在validator中,这样剥离了校验的具体逻辑,读者可以不用关心校验的细节

详情参见本文:Validator使用场景

3.【强制】thrift接口参数校验属于横切关注点,应该放在应用服务来做

横切关注点定义:与具体的领域逻辑无关,且在整个系统中,会作为重用模块被诸多服务调用。调用时,这些关注点是与领域逻辑交织在一起的,因此这些关注点都属于横切关注点

从面向切面编程(AOP)的角度看,所谓“横切关注点”就是那些在职责上是内聚的,但在使用上又会散布在所有对象层次中,且与所散布到的对象的核心功能毫无关系的关注点。

横切关注点的内容包括:事务、监控、身份验证、授权、日志、验证、异常处理等

如何设计应用服务

1.【参考】不包含领域逻辑的业务服务应被定义为应用服务

说明:应用服务为外部调用者提供了一个简单统一的接口,该接口为一个完整的用例场景提供了自给自足的功能,使得调用者无需求助于别的接口就能满足业务需求。对内,应用服务自身并不包含任何领域逻辑,仅负责协调领域模型对象,通过它们的领域能力来组合完成一个完整的应用目标。应用服务作为应用外观,仅仅是领域层的一个入口点,通过它可以降低客户程序与领域层实现之间的依赖

2.【参考】与横切关注点协作的服务应被定义为应用服务

说明:横切关注点的概念参考上文。如果一个服务关心的是横切关注点,那么它就属于一个应用服务。与“横切关注点”对应的是“核心关注点”,就是与系统业务有关的领域逻辑。

例如,订单业务是核心关注点,提交订单时的事务管理以及日志记录则是横切关注点:

代码块

Java

public class OrderAppService {
    @Service
    private PlacingOrderService placingOrderService;

    // 事务为横切关注点
    @Transactional(propagation=Propagation.REQUIRED) 
    public void placeOrder(Order order) { 
        try {
            orderService.execute(order);
        } catch (InvalidOrderException ex | Exception ex) {
            // 日志为横切关注点
            logger.error(ex.getMessage());
            // ApplicationException 派生自 RuntimeException,事务会在抛出该异常时回滚
            throw new ApplicationException("failed to place order", ex);
        }
    }
}

3.【参考】应用服务是协调者,它们只是负责提问,而不负责回答,回答是领域层的工作

说明:站在一个完整用例场景的高度来阐释。当客户端发来请求要执行一个完整的用例场景时,作为协调者的应用服务只负责安排任务,至于任务该怎么做,就是领域模型对象要完成的工作

4.【参考】应用服务中只能包含两部分内容:领域服务协调、横切关注点处理

如此设计自然逃脱不了僵化的嫌疑,但殊不知这是在为设计做减法。若设计者能够充分辨别应用逻辑与领域逻辑之间的差别,突破这一约束也未尝不可。一旦你拥有了足够丰富的设计知识和设计经验,就意味着你可以正确地做出适合当前场景的设计决策与判断。若无法做到,不妨从一些相对固化的简单原则开始做起,这算是从新手到专家所必须经历的成长过程。

4.2.领域层

领域模型对象的哲学依据

领域驱动-补充资料-领域模型对象的哲学依据

实体

实体有自己的生命周期,他的生命周期从属于聚合根。能够以主体类型的形式表达领域逻辑中具有个性特征的概念,而这个主体的状态在相当长一段时间内会持续地变化,因此需要有一个身份标识(Identity) 来标记它。

一个典型的实体应该具备三个要素:

  • 身份标识
  • 属性
  • 领域行为

1.【强制】如果主体拥有唯一标识,业务也要关注主体的状态变化,那么它应该被定义为实体

2.【强制】实体对象的命名以Entity结尾。比如UserEntity、CompanyEntity

3.【参考】Entity<->DO的转换可以放在Entity中,也可以放在单独的convertor中

4.【推荐】Entity<->DO转换建议使用Mapstruct(参考本文【mapstruct对象转换示例】),除非有足够的理由不使用。

5.【参考】并不是所有表结构映射的对象都是实体,也可以是值对象。需要根据领域建模具体分析(区分是否是实体主要通过唯一的身份标识和可变性)

说明:比如小黄卡订单模块,在这个领域中,order就是一个实体,有唯一的身份标识,有自己的生命周期。而订单上的优惠信息,订单类型等都是订单上一些描述信息、属性,他们应该作为值对象。

再比如小黄卡结算系统中,打款单就是一个实体(具有唯一标识、生命周期),而打款单中的一些描述信息就是值对象(金额、时间等)。但在实际中,打款单就是一个对象,可以认为是实体,如果要单独拆分出来,一些描述信息就是值对象

目前系统中存在滥用实体建模的现象,很多时候建模对象是值对象而非实体

6.【推荐】实体尽量使用充血模型。属于实体的行为必须放在实体中,不能放在聚合根中,否则会造成实体贫血

说明:比如小黄卡结算打款单,更新打款单状态,这明显属于实体的行为,应该放在实体中。当然如何划分业务是个难点

值对象

值类型用于度量和描述事物,是否拥有唯一的身份标识才是实体与值对象的根本区别

1.【推荐】应该尽量使用值对象来建模而不是实体对象

2.【强制】当只关心某个对象的属性时,该对象必须定义成一个值对象

3.【强制】我们需要将值对象看成不变对象,不要给它任何身份标识,尽量避免实体对象一样的复杂性

4.【推荐】如何判断是否建模成值对象,建议的判断标准是

  • 它度量或者描述了领域中的一件东西
  • 它可以作为不变量
  • 它将不同的相关的属性组合成一个概念整体
  • 当度量和描述改变时,可以用另一个值对象予以替换
  • 它可以和其他值对象进行相等性比较
  • 它不会对协作对象造成副作用

5.【推荐】值对象数据模型对象转换建议使用mapstruct

6.【强制】值对象的命名规则为 name + Value

说明,比如AddressValue、DescriptionValue、DiscountValue,表示值对象

聚合根

**Eric Evans **阐释了何谓聚合模式:“将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并允许外部对象仅能持有聚合根的引用。作为一个整体来定义聚合的属性和不变量(Invariants),并将执行职责(Enforcement Responsibility)赋予聚合根或指定的框架机制。”

解读这一定义,可以得到如下聚合的基本特征:

  • 聚合是包含了实体和值对象的一个边界
  • 聚合内包含的实体和值对象形成了一棵树,只有实体才能作为这棵树的根,这个根称为聚合根(Aggregate Root),这个实体称为根实体
  • 外部对象只允许持有聚合根的引用,如此才能起到边界的控制作用
  • 聚合作为一个完整的领域概念整体,在其内部会维护这个领域概念的完整性,体现业务上的不变量约束
  • 由聚合根统一对外提供履行该领域概念职责的行为方法,实现内部各个对象之间的行为协作

代表的是一个领域边界,聚合根的内容要保证数据一致性(指业务数据的一致性,包含业务上的业务校验) 比如订单和订单详情,一个没有订单详情的订单是不完整的。聚合根里面有多少个实体,由领域建模决定。将实体和值对象在一致性边界之内组成聚合

实心菱形:合成关系

空心菱形:聚合关系

对象之间的关系:对象之间的关系

建模的原则:

  • 聚合需要维护领域概念的完整性
  • 聚合必须保证领域规则的不变量

说明:不变条件指的是一个业务规则,该规则总是保持一致性的。存在多种类型的一致性,其中之一就是事务一致性。比如在小黄卡订单中,必须要有订单总金额、实际支付金额,优惠明细信息,订单总金额=实际支付金额+优惠金额,这是业务的不变条件

1.【强制】在一个事务中只修改一个聚合实例。如果在单个事务中修改多个聚合,意味着选定的一致性边界是错误的。

2.【推荐】每次客户请求应该只在一个聚合实例上执行一个命令方法

说明:通常情况下不变条件的建模代价并不大,所以设计小的聚合是可能的

3.【推荐】尽量将聚合建模成单个实体-根实体

说明:小聚合不仅有性能和可伸缩性的好处,还有助于事务的成功执行(减少事务提交冲突)。在一般的情况下,需要设计大聚合的不变条件约束不不多,遇到这种情况时,可以考虑添加实体。但无论如何,聚合应设计的尽量小

~4.【推荐】通过唯一标识引用其他聚合。这种关联方式依赖程度是比较小的~

~说明:一个聚合可以引用另一个聚合的根聚合,但是被引用的聚合不能放在引用聚合的一致性边界之内~

领域服务

领域服务(Domain Service)代表了在名词世界(面向对象)中对动词的封装(接口),它封装了领域行为。表示一个无状态的操作

前提在于,这一领域行为在实体或值对象中找不到容身之处。换言之,当我们针对领域行为建模时,需要优先考虑使用值对象和实体来封装领域行为,只有确定无法寻觅到合适的对象来承担时,才将该行为建模为领域服务的方法。

1.【强制】确保领域服务是无状态的。

说明:对单次请求的处理,不依赖其他请求,也就是说,处理一次请求所需的全部信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身不存储任何信息

2.【参考】在与另一个界限上下文交互时,领域服务可以作为RPC的客户端,进行远程调用

3.【强制】领域服务禁止作为远程服务提供方

4.【参考】当领域中的某个操作过程或转换过程不是实体或是值对象的职责时,此时我们应该将该操作放在领域服务中

5.【推荐】在以下情况下,可以使用领域服务

  • 执行一个显著的业务操作过程
  • 对领域对象进行转换
  • 以多个领域对象作为输入进行计算,结果产生一个值对象

6.【强制】领域服务的命名采用【服务名】+ Service的方式,不能为领域服务定义接口

7.【强制】明显属于实体对象或者值对象的行为不应放在领域服务中,否则会导致领域对象贫血

领域事件

发生在领域中的一些事件,将领域中所发生的活动建模成一系列的离散事件,每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中过去发生的事情。

1.**【推荐】**领域事件的命名采用【领域】+ Event的方式

工厂

当创建一个对象或者aggregate时,如果创建过程很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装,隐藏对象创建的细节。

应该将创建复杂对象的实例和aggregate的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建aggregate时要把他作为一个整体,并确保它满足固定规则

1.【强制】每个创建方法都是原子的。

说明:Factory生成的对象要处于一致的状态,无法正确创建出这个对象时,应该抛出异常或者返回null,确保不会返回错误的值

2.【推荐】Factory应该被抽象为所需的类型,而不是所要创建的具体类

说明:针对创建具有多态的行为,工厂接口申明为基类,调用时返回具体类。简单的create不需要遵守

代码块

Java

public interface CouponFactory {
	Coupon create();
}

public class WMCouponFactory implements CouponFactory {
  @Overide
	public Coupon create() {
  	return new WMCoupon();
  }
}

3.【参考】Factory将于其参数发生耦合,如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数的处理。如果只是简单的将参数插入到要构建的对象中,则依赖是适中的。

领域对象工厂示例:

在Factory中,主要的工作就是组装一个Model出来

SchemaFactory

代码块

 public SchemeModel create(Scheme scheme) {
    if(scheme == null){
      return null;
    }
    //如果有变更需要先merge
    if (scheme.getStatus() != null && SchemeStatusEnum.CHANGE.getStatus().equals(scheme.getStatus())) {
        List<PoiChange> poiChanges = poiChangeService.findCurrentChangesBySchemeId(scheme.getId());
        poiChangeService.mergeChange(scheme.getPoiIds(), scheme.getPois(), poiChanges);
    }
    SchemeModel schemeModel = new SchemeModel();
    //填充基本信息
    fillBaseInfo(schemeModel, scheme);
    //填充合作信息
    fillCooperation(schemeModel, scheme);
    if (scheme.getId() != null) {
        //不是新建,则填充门店信息
        fillPois(schemeModel, scheme);
    }
    return schemeModel;
} 

代码中主流程很容易就能看出来,

  1. **根据方案状态进行merge **
  2. 填充基本信息
  3. 填充合作信息
  4. 填充门店信息
  5. 返回

对于一些填充的细节,则需要遵守一些效率上的规定,例如不能循环调用远程服务,不能循环读库,尽量批量处理等。

聚合工厂示例:

工厂实现:

@Component
public class RemitQueryRootFactory {

	@Resource
	RemitFlowDetailRepository remitFlowDetailRepository;

	@Resource
	RemitConverter remitConverter;

	@Resource
	MssFacade mssFacade;

	@Resource
	SinaiFacade sinaiFacade;

	@Resource
	private SettleQueryFacade settleQueryFacade;

	public RemitQueryRoot create() {
		RemitQueryRoot remitQueryRoot = new RemitQueryRoot();
		remitQueryRoot.setRemitFlowDetailRepository(remitFlowDetailRepository);
		remitQueryRoot.setRemitConverter(remitConverter);
		remitQueryRoot.setMssFacade(mssFacade);
		remitQueryRoot.setSinaiFacade(sinaiFacade);
		remitQueryRoot.setSettleQueryFacade(settleQueryFacade);
		return remitQueryRoot;
	}
}

调用:

RemitQueryRoot remitQueryRoot = remitQueryRootFactory.create();

Remote 层

Remote层外部RPC调用规范

1.【推荐外部RPC中申明对象只存在于Remote层,不能扩散到业务层

2.【推荐】Remote方法入参为业务BO对象,不使用RPC层对象

3.【推荐】对rpc返回结果转换成业务BO,再作为方法返回值

Remote层异常处理

| 方案 | 优点 | 缺点 | 推荐 |
| - | - | - | - |
| 异常上抛 | 1.上层能明确知道调用结果(异常)2.上层能获取具体的异常信息 | 1.上层调用facade需要捕获异常 | 推荐 |
| 捕获异常 | 上层不需要捕获异常 | 1.上层不知道调用明确结果,只能根据返回值分析2.发生异常上层没有具体异常信息 | |

例:

4.3.基础设施层

基础设施的职责是为应用程序的其他部分提供技术支持。

资源库

1.【强制】** repository的实现需要放在基础设施层,接口申明在领域层

抽象的资源库接口代表了领域行为,应该放在领域层;实现资源库接口的数据库持久化,需要调用诸如 MyBatis 这样的第三方框架,属于技术实现,应该放在基础设施层。

2.【强制】repository的实现层入参出参应该是业务层对象(值对象或者实体),不能直接使用DO

说明:对象转换推荐使用mapstruct。转换示例

利用mapstruct定义转换的Interface:

代码块

@Mapper
public interface RemitFlowDetailMapper {
	List<RemitFlowDetailEntity> posToEntities(List<RemitFlowDetailPO> pos);
	RemitFlowDetailEntity poToEntitity(RemitFlowDetailPO pos);
}

调用方式:

避免手写对象转换逻辑,减少出错概率,提高效率

3.【推荐】DAO层代码生成推荐用**mybatis-generator,**该插件生成的example能满足大部分的sql需求,能很大程度上避免手写SQL的情况,提高开发效率

一份generatorConfig.xml配置参考:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <context id="MysqlTables" targetRuntime="MyBatis3">

        <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>

        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
            <property name="javaFileEncoding" value="UTF-8"/>
        </commentGenerator>

        <!--数据库链接地址账号密码-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://ip:5002/crayfish?autoReconnect=true" userId="user"
                        password="pwd">
        </jdbcConnection>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
            <property name="TINYINT" value="java.lang.Integer"/>
        </javaTypeResolver>

        <!--生成Model类存放位置-->
        <javaModelGenerator targetProject="src/main/java"
                            targetPackage="com.xxx.xx.settle.infrastructure.repository.persist.model">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="false"/>
        </javaModelGenerator>

        <!--生成映射文件存放位置-->
        <sqlMapGenerator targetProject="src/main/resources/mybatis/" targetPackage="mapper">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--生成Dao类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetProject="src/main/java"
                             targetPackage="com.xx.xx.settle.infrastructure.repository.persist.mapper">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--生成对应表及类名-->
        <table tableName="remit_poi_summarize" domainObjectName="RemitPoiSummarizePO">
                    <generatedKey column="id" sqlStatement="MySql" identity="true"/>
                </table>
    </context>
</generatorConfiguration>

生成的常用方法有:

// 多条件计算总数
long countByExample(RemitExecuteRecordPOExample example);
// 多条件删除
int deleteByExample(RemitExecuteRecordPOExample example);
// 主键删除
int deleteByPrimaryKey(Integer id);
// 插入
int insertSelective(RemitExecuteRecordPO record);
// 多条件查询
List<RemitExecuteRecordPO> selectByExample(RemitExecuteRecordPOExample example);
// 主键查询
RemitExecuteRecordPO selectByPrimaryKey(Integer id);
// 多条件更新
int updateByExampleSelective(@Param("record") RemitExecuteRecordPO record, @Param("example") RemitExecuteRecordPOExample example);
// 主键更新
int updateByPrimaryKeySelective(RemitExecuteRecordPO record);

上面方法能覆盖常用的单表sql操作。避免手写SQL。上层可以根据业务封装不同的方法,底层复用。

ps:多表关联、分组查询不支持,需要手写sql实现。

开放主机服务

http

调用应用层服务进行业务逻辑处理,不能在该层写业务逻辑,这一层应该尽量薄

thrift

这一层调用应用服务进行业务逻辑处理(也可以跨层调用领域服务/聚合根/资源库),避免在该层写入业务逻辑。

几条规定如下:
1.【参考】一般来说调用应用层服务进行业务逻辑处理,但也允许跨层调用(调用领域服务/聚合根/资源库)

2.【强制】不能在该层写入业务逻辑

3.【参考】thrift层Request、response对象可以在应用服务层转换为业务模型,但是在领域层,不能传入thrift 接口定义的对象

通用配置

工程中的通用配置,包括但不限于

  • thrift client、server端的配置
  • MyBatis 配置
  • 消息队列配置
  • 缓存配置

基础服务/工具类

工具类,基础服务的提供

5.场景举例

1.Validator使用场景

通过抛异常形式的validator,在发现某个参数出错之后,直接抛出异常,并带上对应的错误信息。这样相较于返回布尔值的validator来说,所携带的信息量更大。

切记:Validator当中应该只有一个(或多个不同类型的)validate方法,不应该出现其他的方法。

CompanyParamValidator

public String validate(LeasingCompany company, LeasingCompanyFields checkFields) {
        boolean allFlag = checkFields.isAll();
        if (allFlag || checkFields.isBizType()) {
            if (company.getBizType() == null) {
                return "业务类型不能为空";
            }
        }

        if (allFlag || checkFields.isCityId()) {
            if (company.getCityId() <= 0) {
                return "公司注册城市不能为空";
            }
        }

        String cityName = company.getCityName();
        if (allFlag || checkFields.isCityName()) {
            if (StringUtils.isEmpty(cityName)) {
                return "公司注册城市不能为空";
            }
        }

        String companyName = company.getCompanyName();
        if (allFlag || checkFields.isCompanyName()) {
            if (StringUtils.isEmpty(companyName)) {
                return "租赁公司名称不能为空";
            }
            if (VerifyParamUtil.count(companyName) > 50) {
                return "租赁公司名称不能超过50个字符";
            }
            boolean isExistSameCompanyName = isExistSameCompanyName(company.getId(), company.getCompanyName());
            if (isExistSameCompanyName) {
                return "租赁公司名称已存在";
            }
        }
}

2.RPC调用场景

进行rpc调用,异常处理、日志记录、mock等

BannerServiceFacade

代码块

public List<BannerDetail> getBanners(BannerParam bannerParam) {
		BannerResponse bannerResponse = null;
		try {
			bannerResponse = promotionService.getBannerByParam(bannerParam);
		} catch (TException e) {
			LogUtil.sendErrorLog(LogTypeEnum.RPC_ERROR, METHOD[0], bannerParam, e);
			throw new CommonRPCException(APICode.ACTIVITY_RPC_ERROR, false);
		}

		//日志
		LogUtil.sendInfoLog(LogTypeEnum.RPC, METHOD[0], new TwoEntry(bannerParam, bannerResponse));

		CommonAssert.rpcRequireNonNull(bannerResponse, APICode.ACTIVITY_RPC_ERROR);
		CommonAssert.rpcCodeMatch(bannerResponse.getCode().getValue() != ReturnCode.SUCCESS.getValue(), APICode.ACTIVITY_RPC_ERROR);
		return bannerResponse.getBannerDetailList();
	}

3.mapstruct对象转换示例

mapstruct对象转换示例

对象转换采用mapStruct,官网:https://mapstruct.org/

优点:

  • 编译期生成代码。使用时是简单的方法调用,没有反射的过程
  • 编译期类型安全性
  • 功能强大,基本能实现所有的对象转换需求
  • 字段名称相同时自动转换,避免手工转换出错

引入依赖:

<!-- mapstruct-->
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.3.0.Final</version>
</dependency>

 <dependency>
    <groupId>org.mapstruct</groupId>
		<artifactId>mapstruct-processor</artifactId>
		<version>1.3.0.Final</version>
</dependency>

1.生成普通的对象,转换方法为静态方法

接口申明:

@Mapper
public interface RemitFlowDetailMapper {
	RemitFlowDetailMapper INSTANCE = Mappers.getMapper(RemitFlowDetailMapper.class);
	List<RemitFlowDetailPO> entitiesToPos(List<RemitFlowDetailEntity> entities);
	RemitFlowDetailPO entityToPo(RemitFlowDetailEntity entity);
	List<RemitFlowDetailEntity> posToEntities(List<RemitFlowDetailPO> pos);
	RemitFlowDetailEntity poToEntitity(RemitFlowDetailPO pos);
}

代码块

@Override
    public List<RemitFlowDetailEntity> selectByStatus(Integer status) {
        RemitFlowDetailPOExample example = new RemitFlowDetailPOExample();
        example.createCriteria().andRemitStatusEqualTo(status.byteValue());
        return RemitFlowDetailMapper.INSTANCE.posToEntities(remitFlowDetailPOMapper.selectByExample(example));
    }

2.生成spring bean,可以注入

使用示例:

@Mapper(componentModel = "spring", imports = {Jdk8DateUtil.class}, uses = Translator.class)
public interface RemitConverter {
	// 对象字段完全一致,自动转换
	RemitQueryModel convert2Model(QueryRemitReq queryRemitReq);

  // 字段映射自定义
	@Mappings({
			@Mapping(target = "remitSuccessTime", expression = "java(remitFlowDetailPO.getRemitSuccessTime().getTime())")
	})
	RemitDetailDTO convert2DTO(RemitFlowDetailPO remitFlowDetailPO);

  // 支持表达式转换
	@Mappings(
			@Mapping(target = "remitDate",
					expression = "java(Jdk8DateUtil.toLocalDateFormat(remitDownloadModel.getRemitDate().getTime(), \"yyyy/MM/dd\"))")
	)
	RemitDetailDTO modelConvert2DTO(RemitDownloadModel remitDownloadModel);
  
  // 自定义方法扩展
   @Mappings({
            @Mapping(target = "carType", source = "carType", qualifiedByName = {"TagIdTranslator", "manageConfCarTypeEnum"}),
            @Mapping(target = "sponsor", source = "sponsor", qualifiedByName = {"TagIdTranslator", "manageConfRoleEnum"}),
            @Mapping(target = "commentObject", source = "commentObject", qualifiedByName = {"TagIdTranslator", "manageConfRoleEnum"}),
            @Mapping(target = "utime", source = "utime", qualifiedByName = {"TagIdTranslator", "dateToLong"})
    })
    TagGroup ugcTagGroupModelToVO(UgcTagGroupModel model);
}

参考文档:

领域驱动设计实践合订版(战略+战术)--张逸

《实现领域驱动设计》

《领域驱动设计:软件核心复杂性应对之道》

《阿里巴巴Java开发规范手册》


标题:领域驱动设计工程规范(参考)
作者:guobing
地址:http://www.guobingwei.tech/articles/2020/11/07/1604684891200.html