1

前言

在《重构》这本书中,提到了很多种的代码的坏味道,有一种就是重复的代码,以及各种各样的Switch 与 if/else 判断,面对这种情况,可以利用 java 的多态来进行替换。

今天要讲的模板方法就是其中一种利用多态减少重复代码的手段~

注:文中代码片段在实际项目中均已废弃,不过毕竟与业务需求相关,因此代码片段仅保留与模板方法相关的部分,不保证代码片段的实际运行

业务场景

以往我们的修改资源属性和路由时,都是实时生效的,改了就是改了。

那现在用户有了这么一种需求,我的路由修改时,不及时生效,当用户确认修改时再生效,过程中不满意还可以回滚到属性与路由关系最开始的状态。

我们将这种操作称为流程中电路(其实这里比较类似于Oracle自身的回滚操作实现

那么这种需求该怎么实现呢?

以电路资源为例,电路的这种路由关联关系存储于 电路路由表中,我们再搞个历史路由表,专门用来存放最初始的路由关系状态。只要确保每次修改资源时,都肯定已将最初始状态缓存入历史路由表中即可。

这样即可确保路由资源在修改时,其原始信息不会丢失。

一句话总结:确保每次修改路由 / 属性时,都已将相关信息备份。

最初的需求:

围绕上述业务场景,我们来看看最开始的需求:

最开始仅仅要求了电路资源的流程需求,在电路资源的路由实现角度而言,分为这么几个步骤:

  1. 将电路路由关系写入回滚表
  2. 将当前路由关系表中记录删除
  3. 将各个路由资源的属性写入回滚表
  4. 将路由表中涉及的资源状态都设置为流程中的修改状态

解决方案:

来让我们看看代码:

public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) {
   int result = ResCommConst.ZERO;
   String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID);
   String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE);
   // 根据工单与事务状态决定是否走流程中处理逻辑
   if(StringUtil.hasText(woId)&&
         (ResDictValueConst.MNT_ADD.equals(recordOprType)
         ||ResDictValueConst.MNT_DELETE.equals(recordOprType)
         ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){
      
      // 1、将电路路由表的数据写入到回滚表
      List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>();
      Map<String, String> column = new HashMap<String, String>();
      column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CIRCUIT_ID);
      column.put(ResAttrConst.COLUMN_VALUE, identify.getResId());
      columnInfo.add(column);
      trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CIRCUIT_ROUTE, woId);

      // 2、带工单删除只需要更新路由的工单和事物状态
      Map<String, Object> params = new HashMap<String, Object>();
      params.put(ResAttrConst.CIRCUIT_ID, identify.getResId());
      params.put(ResAttrConst.WO_ID, woId);
      params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE);
      circuitDao.updateTrsCirRoute(params);

      // 3、路由表中资源实例写入到回滚表中并更新路由状态
      for (OperationResEntity route : routes) {
         result++;
         intelligentWriteResHis(woId, route);
         route.addOrUpdateProperty(ResAttrConst.WO_ID, woId);
         route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE);
         route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE);
         route.updatePropertys();
      }
   }
   return result;
}

一开始这样写其实没什么问题,代码一共不到 40 行,同时相对清晰的实现了功能需求。

如果需求就是这样,后期维护成本不怎么高,其实没什么改的必要(我比较懒。。。)

进阶需求:

然而生活与工作中却总是 “惊喜” 多过平淡,挫折多过顺风。那该怎么办?日子还是要过,积极面对呗

这不,客户又来了个需求时,不仅电路要这样做,电路的路由——通道也需要支持这种流程中操作。

由于通道自身也有路由,所以其实上述相同的代码逻辑通道也需要实现一份。

解决方案1——常规模式:

先来看看常规的代码是写的呢?

ChannelDataOperation.java

public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) {
   int result = ResCommConst.ZERO;
   String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID);
   String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE);
   // 根据工单与事务状态决定是否走流程中处理逻辑
   if(StringUtil.hasText(woId)&&
         (ResDictValueConst.MNT_ADD.equals(recordOprType)
         ||ResDictValueConst.MNT_DELETE.equals(recordOprType)
         ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){
      
      // 1、将电路路由表的数据写入到回滚表
      List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>();
      Map<String, String> column = new HashMap<String, String>();
      column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CHANNEL_ID);
      column.put(ResAttrConst.COLUMN_VALUE, identify.getResId());
      columnInfo.add(column);
      trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CHANNEL_ROUTE, woId);

      // 2、带工单删除只需要更新路由的工单和事物状态
      Map<String, Object> params = new HashMap<String, Object>();
      params.put(ResAttrConst.CHANNEL_ID, identify.getResId());
      params.put(ResAttrConst.WO_ID, woId);
      params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE);
      channelDao.updateTrsChannelRoute(params);

      // 3、路由表中资源实例写入到回滚表中并更新路由状态
      for (OperationResEntity route : routes) {
         result++;
         intelligentWriteResHis(woId, route);
         route.addOrUpdateProperty(ResAttrConst.WO_ID, woId);
         route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE);
         route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE);
         route.updatePropertys();
      }
   }
   return result;
}

乍一看是不是都一样呢?其实在第16行,19行,26行还是能发现少许不同的。

小结

这种开发方式,其实就是我们最常见的 Ctrl C/V 开发法。

这种开发办法有什么弊端呢?

  1. 传输段也有路由关系,那么如果传输段也要支持流程中操作了,那么是不是又得赋值一套?
  2. 以后我判断是否流程中资源的校验逻辑更改了,那么是不是两处我都得改一遍?
  3. 我不告诉你传输通道也支持了流程操作,那么是不是还需要完整的看一遍通道的代码才能知道哪些资源已经支持了流程操作呢?
  4. 复制容易出错

解决方案二——父类的使用

抛去刚才说的第16行,19行,26行不论,既然其他的代码都是一样的,那我们就先把能抽取的重复代码抽取出来呗~

那么问题来了,抽到哪里?

对于一个类内部的重复代码,我们可以将重复代码抽取到这个类内部的一个独立方法中(ps: IDEA 中抽取方法的快捷键是 ctrl + alt + M)

但是这个例子中,重复代码分散在了不同的类中。所以,我们只能新建一个新类,将重复的方法都放在这个新类中。

HisRouteResDataOperation.java

protected int logRouteAndUpdateState(String woId, List<OperationResEntity> routes) {
    int result = ResCommConst.ZERO;
    for (OperationResEntity route : routes) {
        result++;
        // 1、路由表中资源实例写入到回滚表中
        intelligentWriteResHis(woId, route);
        route.addOrUpdateProperty(ResAttrConst.WO_ID, woId);
        route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE);
        route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE);
        route.updatePropertys();
    }
    return result;
}

如上述代码,我们将电路和通道中完全重复的一段代码抽取成了方法,放在了 HisRouteResDataOperation 中,接下来使电路和通道的操作类继承这个类就可以正常使用了。

接下来看看这时 CircuitDataOperation.java 是怎样的:

public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) {
   int result = ResCommConst.ZERO;
   String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID);
   String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE);
   if(StringUtil.hasText(woId)&&
         (ResDictValueConst.MNT_ADD.equals(recordOprType)
         ||ResDictValueConst.MNT_DELETE.equals(recordOprType)
         ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){
      // 1.0、将电路路由表的数据写入到回滚表
      List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>();
      Map<String, String> column = new HashMap<String, String>();
      column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CIRCUIT_ID);
      column.put(ResAttrConst.COLUMN_VALUE, identify.getResId());
      columnInfo.add(column);
      trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CIRCUIT_ROUTE, woId);

      // 1.1、带工单删除只需要更新路由的工单和事物状态
      Map<String, Object> params = new HashMap<String, Object>();
      params.put(ResAttrConst.CIRCUIT_ID, identify.getResId());
      params.put(ResAttrConst.WO_ID, woId);
      params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE);
      circuitDao.updateTrsCirRoute(params);

      result += logRouteAndUpdateState(woId, routes);
   }
   return result;
}

是不是简化了一点?

这样,我们就将通道和电路的其中两块重复代码提取出来了。

不过同时也可以看到,在第3行,第5行,还有第8行,我们用了 Spring 的注解,留心记一下,这会在后面导致一个小问题。

拓展思考

虽然我们已经将代码中的两部分重复代码移植入父类中,看起来清晰了一点。但是还没有结束,我们发现其实电路和通道在写回滚的逻辑上其实挺相似的,

1. 先判断下是否在流程中,
2. 将路由关系写回滚表并更新路由状态,
3. 将路由资源状态写回滚表并更新状态。

禅师:那么我们如果想将这种流程上的先后顺序进行复用,又该怎么办呢?

王小黑:既然大家这么相似,那么将这部分代码直接放入父类中不好吗?

禅师:嗯,小黑你再好好考虑考虑,还记得我们在进阶需求中的常规解决办法中说的吗?

王小黑:我知道了,通道与电路的保存逻辑,在第16行,19行,26行有一些区别!因为这少许的不同(其实就是我们常说的硬编码),所以我们不能直接将方法抽取到父类中。

禅师:嗯,很好,那你知道该怎么解决吗?

解决方案三——模板方法登场

前文讲到,由于存在硬编码,我们没有办法直接将代码逻辑移植入父类中。

而模板方法模式专门为此而生,让我们看看该怎么写吧~

版本一

TrsHisRouteResDataOperation.java
······
public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) {
    int result = ResCommConst.ZERO;
    String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID);
    String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE);

    if (StringUtil.hasText(woId) &&
            (ResDictValueConst.MNT_ADD.equals(recordOprType)
                    || ResDictValueConst.MNT_DELETE.equals(recordOprType)
                    || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) {

        // 1、将路由关系表的数据写入到回滚表
        List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>();
        Map<String, String> column = new HashMap<String, String>();
        column.put(ResAttrConst.COLUMN_NAME, getResId());
        column.put(ResAttrConst.COLUMN_VALUE, identify.getResId());
        columnInfo.add(column);
        getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId);

        // 2、更新当前路由关系表中路由记录的工单和事物状态为删除态
        Map<String, Object> params = new HashMap<String, Object>();
        params.put(ResAttrConst.WO_ID, woId);
        params.put(getResId(), identify.getResId());
        params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE);
        updateRouteRecord(params);

        for (OperationResEntity route : routes) {
            result++;

            // 1、路由表中资源实例写入到回滚表中
            trsRouteOperationService.logPropertiesToHis(route.getIdentify(), woId);

            // 2、更新路由状态
            route.addOrUpdateProperty(ResAttrConst.WO_ID, woId);
            route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE);
            route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE);
            route.updatePropertys();
        }
    }
    // TODO wang.xi 解决正常的路由保存历史表逻辑
    return result;
}

/**
 * 钩子方法,更新路由的工单与事务状态
 * @param params 工单与事务状态信息
 */
protected void updateRouteRecord(Map<String, Object> params){};

protected String getRouteTableName(){return ""};

protected String getResId(){return ""};

单纯的看这种饱含业务规则的代码肯定是看不进去的,所以这里我们着重看下第16行,19行,26行。

禅师:前文讲了,这几行里面因为存在硬编码,如果简单的将电路的代码上移至父类中,那么通道资源使用这套代码就会有问题了,小黑,你有什么好办法吗?

王小黑:这个我知道,有个最简单的解决方案,反正电路和通道类内都有类似的方法需求,针对第 26 行,我们在 TrsHisRouteResDataOperation 中编写一个空的 updateRouteRecord() 方法使他能找到这个方法,不报错不就好了吗?子类利用 java 的多态机制,实现一下这个方法就好了。(其他部分雷同)

禅师:嗯,你说的确实有用,上面这几行代码也确实是按照你说的做的。但是这样有个缺点,还是之前说的,如果以后传输段也要拓展呢?采取这种方案,即便传输段没有实现这个方法,方法编译时期也不会报错啊!

王小黑:那我们退一步,还有个解决方案,将这个 updateRouteRecord() 方法定义为抽象方法,这不就解决你说的拓展问题了吗?

禅师:根据 java 的语法,如果你将一个方法定义为抽象方法,那么这个类也必须是抽象类了。

王小黑:抽象类就抽象类,又有什么所谓?

禅师:小黑,too young 了吧~ 你仔细看看第 5 行与第 32 行代码,是不是有个 context 与 trsRouteOperationService 对象? 这两个对象都是 Spring 中动态注入的对象,你可以查查 Spring 动态注入与 java 抽象类的关系,就会绝望的发现,Spring 居然不支持抽象类的自动注入。。。(个中原因,等有机会再介绍 Spring 原理的时候再介绍给大家吧)

王小黑:唉,这也是坑那也是坑,横竖都有问题,那么我们还玩不玩了?

版本二

再仔细思考下刚才示例中的 updateRouteRecord() 方法,我们在父类引入这个钩子方法,就是为了利用 java 的多态机制,使父类能够只关心方法的存在与否,而不用再关心具体的实现。

虽然 Spring 不支持抽象类的自动注入,我们依旧可以进一步灵活运用模板方法模式中的钩子方法思想,将类中所需要的属性,创建好getter 方法作为钩子,这样就不再局限与 Spring 自身的限制了。

新的代码如下:

TrsHisRouteResDataOperation.java

public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) {
    int result = ResCommConst.ZERO;
    String woId = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.WO_ID);
    String recordOprType = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.RECORD_OPR_TYPE);

    if (StringUtil.hasText(woId) &&
            (ResDictValueConst.MNT_ADD.equals(recordOprType)
                    || ResDictValueConst.MNT_DELETE.equals(recordOprType)
                    || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) {

        // 1、将路由关系表的数据写入到回滚表
        List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>();
        Map<String, String> column = new HashMap<String, String>();
        column.put(ResAttrConst.COLUMN_NAME, getResId());
        column.put(ResAttrConst.COLUMN_VALUE, identify.getResId());
        columnInfo.add(column);
        getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId);

        // 2、更新当前路由关系表中路由记录的工单和事物状态为删除态
        Map<String, Object> params = new HashMap<String, Object>();
        params.put(ResAttrConst.WO_ID, woId);
        params.put(getResId(), identify.getResId());
        params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE);
        updateRouteRecord(params);

        for (OperationResEntity route : routes) {
            result++;

            // 1、路由表中资源实例写入到回滚表中
            getTrsRouteOperationService().logPropertiesToHis(route.getIdentify(), woId);

            // 2、更新路由状态
            route.addOrUpdateProperty(ResAttrConst.WO_ID, woId);
            route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE);
            route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE);
            route.updatePropertys();
        }
    }
    return result;
}

/**
 * 钩子方法,更新路由的工单与事务状态
 * @param params 工单与事务状态信息
 */
protected abstract void updateRouteRecord(Map<String, Object> params);

public abstract String getRouteTableName();

public abstract String getResId();

public abstract ResContext getContext() ;

public abstract TrsRouteOperationService getTrsRouteOperationService() ;

以上就是模板方法的全部思想了,希望对大家有所帮助 ^_^

小结

在设计模式中,模板方法应该算是比较简单易懂的了,这是理论上而言。

在实际项目中,我们总会因为各种各样的困难,比如懒惰(别笑,这真的是个很充分的理由),比如对象类型不同,比如某一步方法名不同等等的原因,而无法将其抽象为一个模板方法。

但是不管是因为什么原因,却终究是造成了代码中各种雷同逻辑的冗余。比如更早以前的传输带路由资源(通道,电路等)的保存逻辑。因为从流程上来说,就那么几个:

准备对象 —>
删除路由 —>
验证路由状态并计算序号 —>
保存路由 —>
设置路由状态为占用 —>
刷新 A/Z 端属性信息 —>
刷新文本路由信息 —>
记录日志

试想,这么 8 个流程,换做是你,每个方法得用多少行来实现?同时具有这 8 个流程的资源还有 传输通道,传输电路,传输段三种。

算算开发的复杂度是几乘几呢?后期维护时,流程有变换时,又需要该多少行代码呢?

不过虽然说了这么多,但是传输路由保存的代码并没有使用模板方法,同时也依旧很清晰,至于是怎么做到的,先卖个关子,我们下回再聊。

对了,大家可以围绕今天讲的模板方法先思考一下自己模块的代码中是否也有应该使用模板方法的场景呢~


Sivan
525 声望90 粉丝

行业分两个赛道,一个是ToC,一个是 ToB。