策略模式的学习

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<String, String> map, TaskResult result);
}

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

预处理

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

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

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


使用策略模式的预处理

当然原本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层代码获取数据源的时候,只需要依据业务现选择哪种策略/数据源就可以了,大致的思路如下:

使用策略模式的多数据源获取

剩下的工作就是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<User> queryByName(String bussinessID, String name) {
        this.UserMapperDao(bussinessID).queryByName(name);
    }
    
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容