问题起源
最近接到一个任务,是关于本地音乐文件的解析。程序在起来之后,如何在离线状态下,正确的识别客户端已经存在的音频文件信息。
解决方案
设计了一个本地音频解析的一个模块,模块设计的流程图大致如下
流程也不是非常复杂,但是各个模块之间耦合度比较高,下一个节点的处理结果严重依赖上一个节点处理之后的结果,并且存在一些特殊情况,比如可能任务进来之后在缓存节点就能完成所有任务。
说说责任链
看过很多国内论坛描述多责任链,感觉多较为浅显,多数像是一个if else 或者switch语句多放大版本。我理解多责任链模式,应该是入口是一个任务,但是任务需要多个 模块 (节点)协同工作,任务可能在某一个节点处理完成,后续流程就不在需要继续工作。也可能是完整跑完所有 模块 (节点)后返回。那么该模该模式注定还有一个特点,就是 数据共享 (各节点共同加工处理一份数据),各个 模块 (节点)共享一份数据。
设计
基于上面的描述,整个模式有三个角色(对象)。
- 节点(node)
多个节点按序组成一个任务链条
- 共享数据(bundle)
共享数据其实也就是整个任务需要完成数据
- 链条(chains)
上面说若干个节点组成一个责任链,那么此处的链条其实指的是控制链条上的节点按序工作的一个组织者。
代码实现
node
public abstract class BaseNode<T extends ChainsBundle> {
/**
* 在此处申明责任链上每一个节点的名称,
*
*/
public static class Name {
/**
* 文件遍历节点
*/
public final static String TRAVERSE_FOLDER = "TRAVERSE_FOLDER";
/**
* 缓存查询节点
*/
public final static String QUERY_CACHE = "QUERY_CACHE";
/**
* 音频解析节点
*/
public final static String PARSE_AUDIO = "PARSE_AUDIO";
/**
* 分词节点
*/
public final static String TOKENIZE = "TOKENIZE";
/**
* 创建缓存节点
*/
public final static String CREATE_CACHE = "CREATE_CACHE";
/**
* 更新数据库
*/
public final static String UPDATE_DB = "UPDATE_DB";
/**
* 查询数数据库
*/
public final static String QUERY_DB = "QUERY_DB";
/**
* 文件名LIST TO SCANNER
*/
public final static String TRANS = "TRANS";
}
/**
* 设置节点名称
* @return
*/
public abstract String getName();
/**
* 完成自己部分功课
* @param t
* @return T
*/
public abstract T doWork(T t);
/**
* 允许任何一个节点在完成自己任务之后直接跳出整个责任链
*
* @return
*/
public abstract boolean hasNext();
}
代码内的注释每个抽象方法描述都比较清楚,那么随便看一个具体的实现类的代码,大致如下:
public class CreateCache extends BaseNode{
private CreateCache(){}
public static class Builder{
public CreateCache build(){
return new CreateCache();
}
}
@Override
public String getName() {
return Name.CREATE_CACHE;
}
@Override
public ChainsBundle doWork(ChainsBundle bundle) {
//在此节点创建bundle中对缓存对象
bundle.setCache(new ArrayBlockingQueue<ScannerBean>(Conf.AUDIO_SIZE_LIMIT));
try {
bundle = bundle.getTaskFactory().getExecutor().submit(
new CreateCacheFromDBTask.Builder()
.setBundle(bundle).
build()
).get();
} catch (InterruptedException e) {
e.printStackTrace();
bundle.setError(new Error.Builder()
.setCode(Error.ERROR.CREATE_CACHE_FROM_DB_TASK_INTRRRUPTED_EXCEPTION_ERROR.getCode())
.setDesc(Error.ERROR.CREATE_CACHE_FROM_DB_TASK_INTRRRUPTED_EXCEPTION_ERROR.getDesc())
.build());
} catch (ExecutionException e) {
e.printStackTrace();
bundle.setError(new Error.Builder()
.setCode(Error.ERROR.CREATE_CACHE_FROM_DB_TASK_EXECUTION_EXCEPTION_ERROR.getCode())
.setDesc(Error.ERROR.CREATE_CACHE_FROM_DB_TASK_EXECUTION_EXCEPTION_ERROR.getDesc())
.build());
}
return bundle;
}
@Override
public boolean hasNext() {
return false;
}
}
看到这里这个代码里面有一个getTaskFactory 去提交任务到异步线程中工作,之前有说节点需要按序执行,每个节点之间有一个前后逻辑关系的依赖,但复杂的任务又需要到异步工作线程中去执行,在这里面我用到了java中的经典组合,也就是ExecutorService+Future这对经典组合,保证异步执行的结果可以在需要同步的逻辑内拿到执行结果的回调。索性也贴一下TaskFactory的代码,但是该代码于整个模式关系不太大。
public class TaskFactory {
private static class TaskFactoryHolder {
private static final TaskFactory INSTANCE = new TaskFactory();
}
public static final TaskFactory getInstance() {
return TaskFactoryHolder.INSTANCE;
}
private ExecutorService executorService;
private TaskFactory() {
executorService = Executors.newSingleThreadExecutor();
}
/**
* @return
*/
public ExecutorService getExecutor() {
return executorService;
}
/**
* 销毁线程资源
*/
public void destory() {
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}
//todo add other threadCachePool operate method
这样每一个task其实只需要去实现Callable的抽象接口就行了,具体的执行方法就放到了run()方法中去执行了,但是其结果又可以在线程跑完该方法之后直接return到对应的逻辑内。
chains
这部分的逻辑是整个模式设计的核心,但其代码实现起来是非常的简单的。
public class ChainsCreater {
private static final String TAG = Holder.INSTANCE.getClass().getSimpleName();
private static class Holder {
private static final ChainsCreater INSTANCE = new ChainsCreater();
}
public static final ChainsCreater getInstance() {
return Holder.INSTANCE;
}
/**
* 创建并开启整个责任链循环
*/
public void runChains(ChainsBundle bundle , List<BaseNode> nodes, AudioScannerListener listener) {
if(bundle == null){
listener.onError(new Error.Builder()
.setDesc(Error.ERROR.INIT_NOT_EXECUTE_ERROR.getDesc())
.setCode(Error.ERROR.INIT_NOT_EXECUTE_ERROR.getCode())
.build());
return ;
}
//责任链开始,开辟一个新的List<ScannerBean>
bundle.setList(new ArrayList<ScannerBean>());
for (BaseNode node : nodes) {
Log.i(TAG , "to do list size : " + bundle.getToDoList().size());
Log.i(TAG , "next nodes : " + node.getName());
bundle = node.doWork(bundle);
if (bundle.hasError() || ! node.hasNext()) {
jumpChains(bundle,listener);return;
}
}
jumpChains( bundle , listener);
}
/**
* 处理跳出责任链的情况
* @param bundle
* @param listener
*/
private void jumpChains(ChainsBundle bundle , AudioScannerListener listener){
if(bundle.hasError()){
listener.onError(bundle.getError());
}else {
bundle.removeInvalid();//去除无效数据
bundle.updateCache();//更新缓存
listener.onComplete(bundle.getList());
}
}
}
bundle
bundle 应该是和每个具体落地的业务逻辑绑定比较多的一部分内容,我直接贴出来我写的bundle的代码,比较核心的list对象,也是就是每个节点共同处理的一个数据对象。
public class ChainsBundle {
private static final String TAG = ChainsBundle.class.getSimpleName();
private List<ScannerBean> list;
private Error error;
//数据缓存
public Queue<ScannerBean> cache;
private Context appContext;
private DBManager dbManager;
private TaskFactory taskFactory;
public TaskFactory getTaskFactory() {
return taskFactory;
}
public ChainsBundle setTaskFactory(TaskFactory taskFactory) {
this.taskFactory = taskFactory;
return this;
}
public DBManager getDbManager() {
return dbManager;
}
public void setDbManager(DBManager dbManager) {
this.dbManager = dbManager;
}
public Queue<ScannerBean> getCache() {
return cache;
}
public void setCache(Queue<ScannerBean> cache) {
this.cache = cache;
}
public Context getAppContext() {
return appContext;
}
public void setAppContext(Context appContext) {
this.appContext = appContext;
}
public List<ScannerBean> getList() {
return list;
}
public ChainsBundle setList(List<ScannerBean> list) {
this.list = list;
return this;
}
public Error getError() {
return error;
}
public ChainsBundle setError(Error error) {
this.error = error;
return this;
}
/**
* 当前bundle中待处理的数据
* 该list只能作为辅助使用,实际修改还是要修改bundle.getList()对象
*
* @return
*/
public ArrayList<ScannerBean> getToDoList() {
ArrayList<ScannerBean> todoList = new ArrayList<>();
for (ScannerBean b : this.getList()) {
if (!b.isEffect()) {
todoList.add(b);
}
}
return todoList;
}
/**
* 创建依据当前list中的数据,创建对应的METADATA对象
* 请在责任链创建完成,并且确定bundle中list的个数之后调用
*/
public void createMetaData() {
for (ScannerBean bean : this.getList()) {
if (bean.getMetadata() == null) {
bean.setMetadata(new Metadata());
}
}
}
/**
* 当前bundle中是否执行完毕某个节点产生了错误
*
* @return
*/
public boolean hasError() {
return this.getError() != null && !TextUtils.isEmpty(getError().getDesc()) ? true : false;
}
/**
* 删除当前bundle中依然没有被有效处理的数据
*/
public void removeInvalid() {
for (Iterator<ScannerBean> it = getList().iterator(); it.hasNext(); ) {
ScannerBean bean = it.next();
if (!bean.isEffect()) {
it.remove();
}
}
}
/**
* 更新缓存数据
*/
public void updateCache() {
for (int i = 0; Conf.CACHE_SIZE > 0 && getList() != null&& i < getList().size(); i++) {
ScannerBean bean = getList().get(i);
if (!bean.isEffect()) {
continue;
}
if (getCache().size() >= Conf.CACHE_SIZE) {
getCache().poll();
}
getCache().offer(bean);
}
Log.i(TAG, "update cache , current cache size " + getCache().size());
}
}
细心的童鞋应该发现了,我在每一个node内的doWork()方法内传递的都是一份bundle,返回的结果类型也是一个bundle,这样设计有两个好处,一是保证每个节点共同处理的数据都是同一份数据即同一个内存对象的引用。二是我可以在bundle中仅可能的存放所有节点可能需要的工具类对应的引用。这也是为什么我会把db、task的工厂类放进bundle中的原因。
如何生成一个责任链
如果完整的看懂了上面的描述和代码,那么我们再去创建一个责任链的时候,其实就非常的简单了,无非就是把一个相对复杂的任务,分解成各个模块(节点),最后按序放进一个有序的数据集合再启动我们的责任链即可。下面贴一下之前画的流程图对应的任务的责任链启动的代码,大家感受一下。
//1、创建并按序组织责任链节点
List<BaseNode> nodes = new ArrayList<>();
//添加文件扫描节点
nodes.add(new TraverseFolder.Builder()
.setPath(folderPath)
.build());
//缓存查询
nodes.add(new QueryCache.Builder().build());
//查询数据库
nodes.add(new QueryDB.Builder().build());
//添加音频解析节点
nodes.add(new ParseAudio.Builder().build());
//添加分词节点
nodes.add(new Tokenize.Builder().build());
//数据库更新节点
nodes.add(new UpdateDB.Builder().build());
//2、设置回调,并启动责任链
ChainsCreater.getInstance().runChains(bundle , nodes , listener);
最后哔哔两句,喜欢java这个语言,写的代码越多越喜欢java,换做其他的语言,没有这种面向对象的设计,实现起来可能会难看很多。致敬前人,伟大的设计。
以上,不知道有没有人看,有疑问可以留言,我不一定会看。哈哈