​ 合理的使用设计模式能提高代码的可维护性,但是往往一开始设计开发的时候没有考虑到业务的扩展性,在后期业务扩展的时候,需要在原有代码逻辑上增加新功能,这对开发人员来说无疑是非常痛苦的。刚好接到了在新的业务系统增加导入导出功能开发任务,在了解到系统原有导入导出复杂的逻辑后,决定利用策略模式来重构这一功能,降低开发的复杂度。

系统原有导入导出

​ 系统原有使用异步导入导出方式,导入导出任务提交后,先将任务放到任务队列中去,使用一个线程不断从任务队列中获取待执行的任务交由任务线程池去执行。

精简后的部分代码如下:

FileTaskExecutor.java

// 任务分发器线程内部类
class Boss implements Runnable {
	@Override
	public void run() {
		for (;;) {
			FileTask task = null;
			try {
				task = taskQueue.take();
                // 导入导出任务封装为Worker交由线程池执行
				executor.execute(new Worker(task));
				continue;
			} catch (Exception e) {
				logger.error("Get next file task failed,cause by:{}", e);
			}
		}
	}
}

// 任务执行器线程内部类
class Worker implements Runnable {
	private FileTask task;
	public Worker(FileTask task) {
		this.task = task;
	}
    
	@Override
	public void run() {
		try {
			FileType fileType = FileUtil.getFileType(task.getFileName());
			FileHandler fileHandler = FileHandlerFactory.getFileHandler(fileType);
			switch (task.getType()) {
			case Import:
                // 处理导入
				handleImport(fileType, task, fileHandler);
				commitFileTask(task, TaskResult.Success);
				break;
			case Export:
                // 处理导出
				handleExport(fileType, task, fileHandler);
				commitFileTask(task, TaskResult.Success);
				break;
			}
		} catch (Exception e) {
			// TODO 异常情况下文件任务状态处理
			commitFileTask(task, TaskResult.Failed);
			logger.error("Execute file task failed,cause by:{}", e);
		}
	}
}	

导入关键步骤

根据导入的逻辑,导入主要包括以下几个步骤:

  • 1、读取导入文件,获取文件行内容
  • 2、将文件行内容转换为实体类对象
  • 3、将实体类对象保存到数据库
  • 4、导入失败的对象重新写入文件并注明失败原因

导入代码实现

FileImporter.java

@Override
public void batchImport(FileTask task, List<String[]> rowList, FileHandler fileHandler, ExportInfo resultInfo) throws Exception {
    ImportInfo importInfo = null;
    task.addHandledCount(rowList.size());
    String[] headCells = null;
    switch (task.getDataType()) {
        case Blacklist:
            headCells = FileHead.getImportFailedHead(FileHeader.BLACKLIST);
            importInfo = importBlacklist(task, rowList);
            break;
        case Whitelist:
            headCells = FileHead.getImportFailedHead(FileHeader.WHITELIST);
            importInfo = importWhitelist(task, rowList);
            break;
        case Keyword:
            headCells = FileHead.getImportFailedHead(FileHeader.KEYWORD);
            importInfo = importKeyword(task, rowList);
            break;
        default:
            throw new FileImportException("Invalid data type: " + task.getDataType());
    }
    // 导入失败的记录写入文件,并注明失败原因
    List<String[]> cellsList = importInfo.getFailedList();
    writeResultInfoFile(headCells, cellsList, task, resultInfo, fileHandler);
}

/**
 * 导入黑名单
 * @param task
 * @param rowList
 * @return
 */
private ImportInfo importBlacklist(FileTask task, List<String[]> rowList) throws Exception{
    ImportInfo impInfo = new ImportInfo();
    try{
        // 文件行内容转换为实体类对象
        List<Blacklist> blacklists = converter.parseBlacklist(rowList, task.getParamsMap());
        if (ListUtil.isNotBlank(blacklists))
            impInfo = importDataService.importBlacklist(blacklists, task);

    }catch (Exception e) {
        logger.error("Import blacklist failed,cause by:{}", e);
        impInfo.setResult(Result.Failed);
        throw e;
    }
    return impInfo;
}

ImportService.java

@Override
public ImportInfo importBlacklist(List<Blacklist> blacklists, FileTask task) throws Exception {
	ImportInfo importInfo = new ImportInfo();
	ImportInfoBuild importInfoBuild = task.getImportInfoBuild();
	try {
		// TODO 校验黑名单属性值是否合法
		blacklistRepo.addCachePhoneList(blacklist);
		
		// 更新文件任务
		task.setHandledCount(task.getTotal());
		task.setHanldePercent(task.getCurPercent());
		fileTaskRepo.updateHandlingTask(task);
		
		// 返回导入详情
		importInfo.setResult(Result.Success);
	} catch (Exception e) {
		importInfo.setResult(Result.Failed);
		throw e;
	}
	return importInfo;
}

​ 上面代码是导入逻辑的关键代码,如果我们想新增一种导入类型,就必须扩展FileImporter类的switch分支,增加对应的导入类型和方法,并调用ImportService类中新增的导入方法。这样做的缺点就是随着系统导入类型增多,FileImporter类中的分支越来越多,ImportService类中代码量急剧膨胀,给后期开发和维护带来无尽麻烦。

​ 同时从设计原则层面来说,这样设计实现违背了单一职责开闭原则,所有的导入方法都写入一个类中,后期扩展需要在原有类的基础上进行修改。

使用策略模式重构导入

文件导入类中使用策略工厂获取对应的导入策略类,在策略类中将公共方法抽象为接口,这样在新增一种导入类型时,只需新增策略类并实现抽象方法即可。

FileImporter.java(文件导入类)

@Override
public void batchImport(FileTask task, List<String[]> rowList, FileHandler fileHandler, ExportInfo resultInfo) throws Exception {
    // 根据导入类型使用策略工厂获取对应的导入策略
    ImportFileStrategy importFileStrategy = importFileStrategyFactory.fetchStrategy(task.getDataType());
    ImportInfo impInfo =  importFileStrategy.importFile(task, rowList);
    
    // 导入失败的记录写入文件,并注明失败原因
    List<String[]> cellsList = importFileStrategy.dataConvert(impInfo);
    writeResultInfoFile(headCells, cellsList, task, resultInfo, fileHandler);
}

DefaultImportFileStrategyFactory.java(导入策略工厂类)

public class DefaultImportFileStrategyFactory implements ImportFileStrategyFactory, ApplicationContextAware {

    private static final String STRATEGY_SUFFIX = "ImportFileStrategy";
    private static final Logger logger = LoggerFactory.getLogger(DefaultImportFileStrategyFactory.class);

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public ImportFileStrategy fetchStrategy(String typeName) throws ImportFileStrategyFactoryException {
        ImportFileStrategy importFileStrategy = applicationContext.getBean(typeName + STRATEGY_SUFFIX, ImportFileStrategy.class);
        if (importFileStrategy == null) {
            logger.error("Fetch ImportFileStrategy failed,Unknown strategy type:{}",typeName);
            throw new ImportFileStrategyFactoryException(MessageFormat.format("Fetch ImportFileStrategy failed,Unknown strategy type:{0}",typeName));
        }
        return importFileStrategy;
    }
}

ImportFileStrategy.java**(导入策略接口)

public interface ImportFileStrategy<E> {

    ImportInfo importFile(FileTask task, List<String[]> rowList) throws Exception;

    String[] getImportFileHeader();

    List<String[]> dataConvert(List<E> elements);
}

AbstractImportFileStrategy.java(导入策略抽象类)

public abstract class AbstractImportFileStrategy<E> implements ImportFileStrategy<E> {

    @Override
    public ImportInfo importFile(FileTask task, List<String[]> rowList) throws Exception {
        Map<String, String> params = task.getParamsMap();
        List<E> elements = rowList2Objects(task, rowList, params);
        return importList(task, elements);
    }
    
    protected abstract List<E> rowList2Objects(FileTask task, List<String[]> rowList, Map<String, String> params);
    protected abstract ImportInfo importList(FileTask task, List<E> elements) throws Exception ;

BlacklistImportFileStrategy.java(黑名单导入策略类)

public class BlacklistImportFileStrategy extends AbstractImportFileStrategy<Blacklist {
    @Override
    protected List<Blacklist> rowList2Objects(FileTask task, List<String[]> rowList, Map<String, String> params) {
        // TODO 文件行内容转换为实体类对象
        return blacklist;
    }
    @Override
    protected ImportInfo importList(FileTask task, List<Blacklist> elements) throws Exception {
        // TODO 将对象列表存储到数据库
        return importInfo;
    }
    @Override
    public String[] getImportFileHeader() {
        // 获取文件头信息
        return header;
    }
    @Override
    public List<String[]> dataConvert(List<Blacklist> elements) {
        // 将类对象转换为文件行内容
        return list;
    }
}

从上我们可以看出,当我们需要新增一个导出类型时,只需新增一个导出策略类并继承AbstractImportFileStrategy抽象类,实现所有抽象方法即可。

系统导出的问题与导入类似,重构的方法也是类似的,这里不再赘述。