模板方法是一个十分常见的设计模式,它并不复杂但是它带来的好处就是定义流程框架,而不关注实现细节,前两天看到一句话又回味了一把之前很欣赏的面向对象设计思想,接口和抽象类的概念就是能够让外部依赖的东西尽量抽象,依赖抽象而不依赖细节,这样更容易开发出高内聚低耦合的系统。
模板方法具体如何实现这种流程编排,屏蔽细节的效果呢。
有几个角色
- 模板类:它一般定义一套比较完整的流程,这些流程定义叫做模板方法,它的一些具体实现由抽象方法或则可被重写的空实现组成,这个方法我们叫做钩子方法,把能够明确的流程给确认下来。
- 具体实现类:它扩展重写了钩子方法,实现了细节而不关注流程。
我举个柱体体积与面积计算的例子去模拟模板模式的使用场景,首先定义一个接口:
/**
* 柱体体积面积 interface
*/
public interface ShapeProcess {
double getArea();
double getVol();
}
接口能够让外部能够通过接口与对象交流,不关心具体的实现了。
/**
* 模板
*/
public abstract class AbstractShap implements ShapeProcess {
// 模板方法
@Override
public final double getVol() {
return getArea() * getHigh();
}
// 钩子方法
protected abstract double getHigh();
}
因为柱体的体积都是底面积乘以高,但是底部面积却是千变万化,这个抽象类定义体积的算法的模板,但是获取体积还需要高,我们就定义一个钩子方法即可。
public class Square extends AbstractShap implements ShapeProcess {
private int width;
private int len;
private int high;
public Square(int width, int len, int high) {
this.width = width;
this.len = len;
this.high = high;
}
// 底部面积自己实现
@Override
public double getArea() {
return width * len;
}
// 实现钩子
@Override
protected double getHigh() {
return this.high;
}
}
具体的方形实现自己的面积并实现了钩子方法就能达到目的。以后新的图形直接继承模板方法就可以实现编排好的获取体积,只需要实现各自的面积和并返回高即可。
在项目中,我们很多时候要把批量查询的需求拆分成多个子任务并行去查询,我写了一个简单的执行框架:
public abstract class BatchExecuteService<P, R> {
protected ExecutorService getExecuteService() {
return Executors.newCachedThreadPool();
}
public List<R> submit(List<P> paramsList) {
List<List<P>> subParamsList = Lists.partition(paramsList, getSplitSize());
List<Future<List<R>>> futureTask = Lists.newArrayList();
ExecutorService executorService = getExecuteService();
for (List<P> subParams : subParamsList) {
Callable<List<R>> task = getTask(subParams);
Future future = executorService.submit(task);
futureTask.add(future);
}
List<R> resultList = Lists.newArrayList();
for (Future<List<R>> future : futureTask) {
try {
if (future.get() != null)
resultList.addAll(future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
return resultList;
}
public abstract Callable<List<R>> getTask(List<P> paramsList);
protected Integer getSplitSize() {
// default
return 10;
}
}
子类实现具体要执行的任务:
public class TestBatchExecuteService extends BatchExecuteService<Integer, Integer> {
@Override
public Callable<List<Integer>> getTask(List<Integer> paramsList) {
return () -> {
try {
Long t1 = System.currentTimeMillis();
System.out.println("start thread, threadName : " + Thread.currentThread().getName());
Thread.sleep(2000);
Long t2 = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " cost " + (t2 - t1));
return Arrays.asList(new Integer[]{1,2});
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
};
}
@Override
protected Integer getSplitSize() {
return 2;
}
public static void main(String args[]) {
TestBatchExecuteService testBatchExecuteService = new TestBatchExecuteService();
List<Integer> integerList = Lists.newArrayList();
for (int i = 0;i < 10;i ++) {
integerList.add(1);
}
testBatchExecuteService.submit(integerList);
System.out.println("执行完毕");
}
}
总结
可以发现无论是哪种设计模式,都是封装变化,隔离稳定与变化,很多时候我们写代码虽然用了对面向对象支持良好的语言却在用面向过程在编写程序,这样不能体现出面向对象的优势,无论是框架还是类库,它都是把能够固定的过程给写下来,让扩展的部分留给了应用程序开发者去编写,传统的面向过程的编程语言使用的是早期绑定策略,就是让应用开发者去调用类库,其实面向对象更适合类库定义虚函数来调用应用程序的开发者,这叫做晚期绑定,这样把流程编排更多放在框架里,在降低开发应用程序门槛的同时也导致很多时候应用程序不清楚框架做了什么事情。所以Java有入门容易进阶比较难的说法,但是如果巧妙的运用设计模式,就可以站在框架甚至架构的角度去考虑系统。
而回到设计模式最根本的目的就是分离变化与稳定,这才是最难的部分。因为需要依赖抽象思维,这与计算机的具体思维是不同的。还需要对设计模式有理解,也需要对业务场景有相对比较深入,这样才能把握变与不变。