策略模式的学习

1. 问题来源

在年初看某业务线清结算部分代码时,遇到这么一串代码,大致如下:


//如果商户文件到达,使用ftp下载文件
if (map.get(SystemConstant.SYSTEM_TYPE).equals("2")) {
    try{
            ... ...

        ApacheFTPUtil plugins=new ApacheFTPUtil();
        plugins.execute(paramMap);//下载文件路径信息在paramMap中

        businessLogger1.info(... + "FTP文件下载结束");
    }catch(Exception e){
        ... ...
    }
}
//如果文件下载到文件核心完成
if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
    businessLogger1.info(... + "文件到达检测启动");

    BaseComponentPlugins plugins = new FileArrive();//调用文件级实现
    result = new TaskResult();
    plugins.execute(map, result);

    businessLogger1.info(... + "文件到达检测结束");
    
    //文件级检测完成且没有问题,开始进行记录级检查
    if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
        businessLogger1.info(... + "文件校验启动");
        result = new TaskResult();

        plugins = new FileValid();
        plugins.execute(map, result);//调用记录级实现

        businessLogger1.info(... + "文件校验结束");

        //文件记录入库
        if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
            businessLogger1.info(... + "异步入库启动");
            result = new TaskResult();

            plugins = new FileEnterDB();
            plugins.execute(map, result);//调用异步入库实现

            businessLogger1.info(... + "异步入库结束");

            //入库HDFS
            if (result.getIsSuccess() == TaskResult.TASK_EXEC_SUCCESS) {
                businessLogger1.info(map, "入库HDFS开始");
                result = new TaskResult();
                
                plugins = new HdfsLoad();
                plugins.execute(map, result);//调用入库HDFS实现

                businessLogger1.info(map, "入库HDFS结束");
            }else{
                ... ...//入库HDFS异常
            }           
        }else{
            ... ...//文件记录入库异常
        }
    }else{
        ... ...//文件记录级入库/异步校验异常
    }
}else{
    ... ...//文件级校验异常
}

额外说明的是:

  • 由于清算步骤前后因果关系,必须前一步执行成功才能继续下一步,所以代码结构是这样多个if嵌套的样子
  • 哪一步失败就需要立即停止并反馈到日志或者数据库,各个else的代码也就是处理这些异常的,此处也没有列出。

代码中多次使用到了这样的结构:

BaseComponentPlugins plugins = new ... ...
plugins.execute(map, result);

可能熟悉策略模式的人早就明白这是怎么回事了,但是我刚开始看得时候就只是觉得,简洁明了、维护和可读性都很高,再就是感叹接口原来是这么用的。

2. 策略模式是什么?

最近在看资料的时候,偶然看到几篇介绍策略模式的文章,才慢慢理解了。回顾到这段代码的时候,也才渐渐明白这就是策略模式的实现。
策略模式的理解可以参见这篇文章,例子生动形象:【行为型模式十五】策略模式(Strategy)
我的理解就是把一个个具体的算法和使用算法的类是进行解耦,比如结算业务的例子被拆分成:

  • FTP下载文件
  • 文件级校验
  • 记录及校验
  • 异步入库
  • 入库HDFS

每种操作都单独形成一个单独的算法类,它们共同实现同一个接口、接收相同参数结构的参数,而具体使用哪一个由使用算法的类来选择。
接口定义如下:


package com.xxxx.commonbase.plugins;

import java.util.Map;

import com.xxxx.platform.bean.TaskResult;

public interface BaseComponentPlugins {
    abstract int execute(Map map, TaskResult result);
}

再通过类图关系可以更清楚的看到算法实现与策略接口的关系:

策略模式的学习_第1张图片
预处理

上图的关系可以看到,这种处理方式很接近策略模式了,但是PretreatmentDeal类中并没有持有策略(也就是BaseComponentPlugins接口)的引用,为了完成结算它还是通过先调用FileArrive、FileValid... ... 这样一步步完成的,只是实例化这些对象使用BaseComponentPlugins引用而已,如之前所述:

BaseComponentPlugins plugins = new ... ...
plugins.execute(map, result);

而真正的策略模式,除了策略接口规范一系列具体的策略算法所应该完成的事;还需要上下文context类要持有策略接口,当调用这个上下文类的对象时,会决定使用哪种策略实现。
如果真的要以策略模式的实现,这个PretreatmentDeal类就必须先有一个策略的成员变量,修改后的类关系图应该如下:


策略模式的学习_第2张图片
使用策略模式的预处理

当然原本PretreatmentDeal类的各种if条件需要抽取形成新的调用类,由这个调用类来判断每一步的结果并决定是否更换策略,PretreatmentDealContext只是简单调用excetu()方法并返回结果。

3.业务线强相关的数据源切换

最近集中管控台需要管理使用多个数据库,数据库的选择只与业务线相关联.在最开始的时候我能想到的代码可能也就是类似下面的if判断形式了.


//依据产品和业务线取不同的数据库资源
if (paramMap.getProduct.equals(SystemConstant.PRODUCT_A)) {
    if (paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_A)) {
        ... ...//取A业务线对应的数据库
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_B)) {
        ... ...//取B业务线对应的数据库
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_C)) {
        ... ... //取C业务线对应的数据库
    }
}else if(paramMap.getProduct.equals(SystemConstant.PRODUCT_B) {
    if (paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_A)) {
        ... ...//取A业务线对应的数据库
    }else if(paramMap.getBussinessLine.equals(SystemConstant.BUSSINESS_LINE_B)) {
        ... ...//取B业务线对应的数据库
    }   
}

这样的写法缺点在策略模式的讲解文章已经谈了无数遍了,所以这个可以考虑修改下代码,抽象出数据源的获取方式/算法,新增一个contex类拿到获取数据源的接口引用。
这样,当service层代码获取数据源的时候,只需要依据业务现选择哪种策略/数据源就可以了,大致的思路如下:

策略模式的学习_第3张图片
使用策略模式的多数据源获取

剩下的工作就是service层依据controller层的参数,做出选择:

  • 依据产品product选择使用哪种策略(即哪种获取数据库资源的方式,比如oracle、mysql),
  • 依据具体业务现选择哪个数据源(比如同一个数ip下不同数据库用户名管理的表等等)
    这样,可以避免大量的判断条件。上下文类DataSourceContext代码写法较为固定,类似这样:

/**
* 数据源管理,完成向调用者返回匹配的数据库资源
*/
public class DataSourceContext {
  /**
   * 持有一个具体的策略对象(数据库资源接口)
   */
  private AccessDataSource AccessDataSource = null;
  
  /**
   * 构造方法,传入一个具体业务线数据库资源对象
   */
  public DataSourceContext(Strategy aStrategy){
      this.strategy = aStrategy;
  }  
  
  /**
   * 获取数据库资源
   * @param bussinessChannel业务线编码
   * @return 业务线关联的数据库资源
   */
  public double getDataSource(String bussinessChannel){
      return this.strategy.getDataSource(bussinessChannel);
  }
}

这么做就具备了很强的灵活性,比如配置不同类型数据库、同一个数据库多个数据库用户的管理;尤其是新增一个业务现,只需新增业务线

4.一点感受

凡事有利有弊,策略模式要求调用者必须清楚地知道每种策略实现,这样才能在选择的时候传递给context类具体的策略。比如这个数据源的例子调用类就必须的写成:


public class ProductAUserServiceImpl {
    
    //选择并创建需要使用的数据源,需要显示声明使用哪一种
    AccessDataSource strategy = new ProductADataSource();
    
    //创建上下文对象
    DataSourceContext context =  new DataSourceContext(strategy);
    
    /**
    * 依据业务线bussinessChannel拿到具体mybatis dao
    */
    public UserMapperDao UserMapperDao(String bussinessChannel){
        return context.strategy.getDataSource(bussinessChannel);
    }
    
    /**
    * 具体查询实现,供给controller层使用
    */
    public List queryByName(String bussinessID, String name) {
        this.UserMapperDao(bussinessID).queryByName(name);
    }
    
}

也有说法可以调用者不需要知道具体哪些策略,完全有context类依据参数控制,没有在继续看,但感觉如果处理不好、context类又要出现一堆if判断,到时又得想办法处理掉这些判断。

你可能感兴趣的:(策略模式的学习)