1

业务中使用依赖方 DTO 类所带来的问题

我负责的系统所依赖的部分服务接口需要重构,把对应的接口从 A 服务迁移到了 B 服务,虽然入参出参格式都一样,但包路径完全变了。而原有的 A 服务仍然有接口依赖,所以我必须要兼容两种【类名一样结构几乎一样!】但【包路径完全不同!】的出入参。
但由于我这边的系统代码结构中业务逻辑一直使用了 A 服务提供的 SDK 中的 DTO 定义来做业务参数,这时候就僵硬起来了。。该怎么办呢?

image

要不自定义一个 BO 吧!

基于目前代码结构来看,自己系统的业务代码中严重依赖了第三方服务 SDK 的定义类,使得自身与第三方服务形成了强耦合的关系(万一人家更新 SDK 中的 BO 定义,自己岂不是得全改?!)。所以第一个想到的就是:不要用人家的 BO 了,我们自定义一个吧!

image

这样无论人家的 SDK 怎么变,我们只需要在转换器中做好映射处理,就可以完美解决强耦合问题了,prefect!
正当这时候定睛一看(兔美酱的眼神突然犀利了起来),这样会涉及到大量的业务逻辑改动(所有使用到对应 BO 的地方都要改成新的)!而双十一大促快到了,这时候做大改动出问题的话无疑是【捡了芝麻丢了西瓜】呀。
自定义 Self.BO 改动太大,风险太高,那么只能先保证原业务逻辑还是使用 A.BO 类,让 B.BO 自行去兼容了。。T.T 

如何优雅降级而后续可扩展?

虽然双十一前需要使用降级方案,但是考虑扩展性,后续还是需要解耦的。所以决定使用以下方案:

image

转化器还是要做,但不是转化成自定义 SELF.BO 类,而是转成 A.BO,这样做我们后续就可以把转化器逻辑,与主业务的 BO 包换回 SELF.BO,即可完成统一转化。这等于我们现在用最小的改动,就可以完成整体解耦的一半工作量了,无疑是当下最优方案。


转化器的实现

先看看 BO 的定义:

public class Bo implements Serializable {
  private String goodsId;
  private String goodsName;
  private String brandStoreId ="";
  private String brandStoreName ="";
  private String categoryId;
  private String status;
  private Map<String, GoodsImgTagVo> goodsImageTags;

  ....... // 还有很多其他属性,就不一一列举了

  ....... // 然后还有一堆 getter() / setter(),也不一一列举了
}
BO 中还有 一个 Map<String, GoodsImgTagVo> 类型的属性,其中 GoodsImgTagVo 也是分别对应 A、B 包。

关于转化器的实现,其实有很多种方法:

1、最先想到的办法肯定是逐个映射:

public static A.BO parseToABo(B.BO bo){
  A.BO aBo =new A.BO();

  // goodsImageTags
  Map goodsImgTagVoMap =null;
  // 转化各种类成返回类
  if(null != bo.getGoodsImageTags()) {
    goodsImgTagVoMap =new HashMap<>();
    for (Map.Entry entry : bo.getGoodsImageTags().entrySet()) {
      GoodsImgTagVo vo =new GoodsImgTagVo();
      vo.setGroup(entry.getValue().getGroup());
      vo.setImage(entry.getValue().getImage());
      vo.setSort(entry.getValue().getSort());
      vo.setTag(entry.getValue().getTag());
      goodsImgTagVoMap.put(entry.getKey(), vo);
    }
  }

  // 把这些对应的属性转化成返回类的属性
  aBo.setGoodsId(bo.getGoodsId());
  aBo.setGoodsName(bo.getGoodsName());
  aBo.setBrandStoreId(bo.getBrandStoreId());
  aBo.setBrandStoreName(bo.getBrandStoreName());
  aBo.setCategoryId(bo.getCategoryId());
  aBo.setStatus(bo.getStatus());
  aBo.setGoodsImageTags(goodsImgTagVoMap);
  return aBo;
}
· 优点:直接使用 java 原生实现,执行效率上应该是最快的。

· 缺点:非常明显,代码量会非常臃肿,而且只能从 B 转化成 A;如果需要增加一个 C,即需要又新增一个方法了~

写完几个类映射后我发现,简直是在刷代码量啊,而且代码重复率会被各种拉高!!

2、如果都是基础类型,可以直接使用 BeanUtils.copyProperties() 方法进行属性复制

在寻找解决办法的过程中,找到这个工具类可以对属性均为基础类型的 DTO 实体类进行快速复制转换,用法大概为:

public static A.BO parseToABo(B.BO bo){
  A.BO aBo = new A.BO();

  //第一个参数是源,第二个参数是目标实体类VO
  BeanUtils.copyProperties(bo, aBo);
  return aBo;
}

该方法可以复制 bo 实例中的所有属性复制到 aBo 的同名属性中。可它这有一个缺陷:只能复制基础类型(String、integer、boolean 这种),如果像我这样 BO 中又包含自定义类型的话,方法将无法正常复制而抛出一个 InvocationTargetException,所以我是无法使用这个方法来快速实现转换了。。(再次哭。。 ToT

3、使用适配器设计模式,实现多维度多类型适配

适配器模式是一种结构型设计模式。适配器模式的思想是:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

例如我有一台只有 Type-C 插口的 macbook 和一个 USB 插头的 U 盘,电脑想使用 U 盘的话就需要一个转接头,此时转接头就是一个适配器。

适配器模式涉及3个角色:

· 源(Adaptee):需要被适配的对象或类型,相当于 USB 插头。

· 适配器(Adapter):连接目标和源的中间对象,相当于转接头。

· 目标(Target):期待得到的目标,相当于 Type-C 插口。

适配器模式包括3种形式:类适配器模式、对象适配器模式、接口适配器模式(或又称作缺省适配器模式)。

image

虽然目前我们处理的都是一些相同属性的不同 DTO 类,但在业务发展的过程中,无法避免要兼容不完全一样的 DTO,这时候我们就需要有一个适配器来适配成统一的接口规范了。

基于上述考虑,我们可以使用适配器模式来统一实现 DTO 类转换:

public class ABoAdapter extends A.BOImpl  {
  private B.BO;
  public ResultAdapter(B.BO bBo){
    this.bBo = bBo;
  }

  @Override
  public String getGoodsId() {
    return this.bBo.getGoodsId();
  }

  @Override
  public void set GoodsId(String value) {
    this.bBo.setGoodsId(value);
  }

  ....... // 继续重写原 A.BO 的所有 getter() setter() 方法
}

当我们使用时就可以直接:

A.BO aBo = new ABoAdapter(bBo);

经典如 Java 中 IO 对 Reader、InputStream之间的适配,字符流、字节流之间的适配,就是使用适配器模式实现,详细实现就不作展开了。

image

这样做的优点是可以对所有(无论内部属性是否一致)的 DTO 类都能写出新的适配器进行转化,大大提高了系统扩展性。

但由于业务代码中声明的都是实体类,无法完全使用适配器模式中使用接口方式进行声明,无法完全解耦。(这个故事教会了我们:参数声明,应尽量使用接口类型)
与此同时带来的还是跟逐一映射差不多的代码量,当拥有许多适配器时,会让系统变得非常零乱。

4、使用 Orika 项目中的 MapperFactory 工具类进行自定义映射(最优解)

我们先来看看 Orika 和 MapperFactory 是什么?

Orika(以前托管于谷歌代码)声称它是一个更简单、更轻量、更快的Java Bean映射工具。

它允许你对象之间的转换通过一个对象的属性值赋值到另外一个对象中,操作使用Java的内部机制,代替传统的XML或者诸如此类的配置。它使用代码生成来创建映射器,并且它的核心类有一个有趣的策略,它允许您优化性能。

它是完全可配置的,例如,默认情况下,java代码生成器是Javassist。但是如果你使用EclipseJdt甚至是使用其他的代码生成器提供者实现适当的接口。最后,这是一个非常有文档的项目,有清晰的解释和有用的代码

通过使用Orika提供的API,你可以构建一个以MapperFactory为主要实现的通道,然后配合DefaultMapperFactory和一些其他的静态类助手用户构建MapperFactory,然后你可以从MapperFactory中获取ClassMapBuilder以便为了流式申明映射配置,计字段映射(默认是双向的,如果你愿意,可以设置成单项的),不包括字段,指定构造函数和自定义列表、集合、嵌套字段等的映射。

有一个默认映射,就是映射两个类之间名称相同的字段,当然你可以在MapperFactory注册自定义映射规则(ClassMapBuild 就是MapperFactory中通过属性来构建的),这两个操作都有合适分方法来调用它们。

Orika 的官方文档在这:http://orika-mapper.github.io/orika-docs/index.html

了解它的能力以后,我们废话不多说,立刻上实现代码:

public class BoParserHelper {
    static MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

    /**
    * 转化 B.BO 为 A.BO
    *
    * @param bo
    * @return A.BO 
    */
    public static A.BO parseToABo(B.BO bo){
        return mapperFactory.getMapperFacade().map(bo, A.BO.class);
    }
}

你没看错,如果只是单纯包路径不同,转化就是一行代码这么简单!

同时 MapperFactory 类还提供自定义的属性名转换,如 

image

image

如果以上规则均不适用自身场景,还能使用 CustomerMapper 来高度定制化自己的映射规则,详看官方文档。

使用 MapperFactory 后,转换器代码从原来的 100+ 行(由于 DTO 类属性非常多)代码缩减到了仅 1 行,而且使得可读性、可维护性和扩展性也得到了很大的增强,所以我们最终采用这种方案进行 DTO 类的转换器实现,可见 Java 工具的博大精深啊~~

不过需要注意的是,MapperFactory 的初始化性能损耗比较大,所以我们在实际进程中最好只初始化一次(可以让 spring 托管自行实例化),避免不必要的性能损耗。

结语

不得不感叹前人对编程思想的极致总结和所有编程人员的无私奉献。使得我们能够得出现在强大的 Java 提供了大量强大的工具库。


sinlong
9 声望0 粉丝