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判断,到时又得想办法处理掉这些判断。