模板方法简介

前言

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

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

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

业务场景

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

那如今用户有了这么一种需求,个人路由修改时,不及时生效,当用户确认修改时再生效,过程当中不满意还能够回滚到属性与路由关系最开始的状态。ui

咱们将这种操做称为流程中电路(其实这里比较相似于Oracle自身的回滚操做实现编码

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

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

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

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

最初的需求:

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

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

  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))){
      
      // 一、将电路路由表的数据写入到回滚表
      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);

      // 二、带工单删除只须要更新路由的工单和事物状态
      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);

      // 三、路由表中资源实例写入到回滚表中并更新路由状态
      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))){
      
      // 一、将电路路由表的数据写入到回滚表
      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);

      // 二、带工单删除只须要更新路由的工单和事物状态
      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);

      // 三、路由表中资源实例写入到回滚表中并更新路由状态
      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++;
        // 一、路由表中资源实例写入到回滚表中
        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.一、带工单删除只须要更新路由的工单和事物状态
      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))) {

        // 一、将路由关系表的数据写入到回滚表
        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);

        // 二、更新当前路由关系表中路由记录的工单和事物状态为删除态
        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++;

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

            // 二、更新路由状态
            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))) {

        // 一、将路由关系表的数据写入到回滚表
        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);

        // 二、更新当前路由关系表中路由记录的工单和事物状态为删除态
        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++;

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

            // 二、更新路由状态
            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 个流程的资源还有 传输通道,传输电路,传输段三种。

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

不过虽说了这么多,可是传输路由保存的代码并无使用模板方法,同时也依旧很清晰,至因而怎么作到的,先卖个关子,咱们下回再聊。

对了,你们能够围绕今天讲的模板方法先思考一下本身模块的代码中是否也有应该使用模板方法的场景呢~

相关文章
相关标签/搜索