What
模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure. 在方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模版方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
- 解释:
这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
作用
模板模式主要是用来解决复用和扩展两个问题。
- 复用
模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分留给子类来实现。所有的子类都可以复用父类中模板方法定义的流程代码。- 应用案例
- Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer;
- Java AbstractList
- 应用案例
- 扩展
这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似控制反转。基于这个作用,模板模式常用在框架的开发中,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
/**
抽象模板类 */ abstract class AbstractClass {
/**
模板方法 */ public final void templateMethod() { //… method1(); //… method2(); //… }
protected abstract void method1();
protected abstract void method2(); }
class ConcreteClass1 extends AbstractClass { @Override protected void method1() { //… }
@Override
protected void method2() {
//...
}
}
class ConcreteClass2 extends AbstractClass { @Override protected void method1() { //… }
@Override
protected void method2() {
//...
}
}
在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
<a name="OZK46"></a>
# Why
<a name="IRYAy"></a>
## 思考问题
- 假设一个框架中的某个类暴露了两个模板方法,并且定义了一堆供模板方法调用的抽象方法。在项目开发中,即便我们只用到这个类的其中一个模板方法,我们还是要在子类中把所有的抽象方法都实现一遍,这相当于无效劳动,有没有其他方式来解决这个问题呢?
- 解1:
在 spring 生命周期中,InstantiationAwareBeanPostProcessorAdapter<br />就是解决这个问题的。<br />写个适配器,把所有抽象方法默认实现一下,子类继承这个 adapter 就行了。
- 解2:
可以借鉴AbstractList的addall实现。提供默认的方法实现(每个方法直接抛出异常),使用模板方法的时候强制重写用到的method方法,用不到的method不用重写。
<a name="CR6rg"></a>
## 回调
> 回调是一种双向调用关系。类A的a()方法调用类B的b()方法,类B的b()方法执行完毕主动调用类A的back()方法。这里的back()方法就被被称为回调函数,**A调用B,B反过来又调用A**,这种调用机制就叫作“回调”。
```java
public class A {
void a(B b){
//调用b()方法,并将A类传递到B类
b.b(this);
}
//回调函数
public void back(){
System.out.println("A类,回调函数执行了!");
}
}
class B{
void b(A a){
//B中回调A的回调函数
a.back();
}
}
生产环境中一般将回调函数抽象成接口(推荐将该接口也可定义在调用类B中),让A类来实现该接口,再依赖注入到B类实例时,通过接口来传递,实现解耦。
匿名回调 ```java public class BClass { //回调接口 interface ICallback {
void back();
}
public void process(ICallback callback) { //… callback.back(); //… } }
class AClass { public static void main(String[] args) { BClass b = new BClass(); b.process(new B.ICallback() { //回调对象 @Override public void back() { System.out.println(“Call back me.”); } }); } }
<a name="dgp4d"></a>
### 回调的两种实现方式
从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。
<a name="xEi2X"></a>
#### 同步回调
在函数返回之前执行回调函数
```java
/**
* 同步回调(经典回调方式)
* 在函数返回之前执行回调函数,从应用场景看,同步回调看起来更像模板模式。
* @author yiy
* @date 12/29/2021
*/
public class SyncBackWay {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.a(b);
}
}
class A implements B.ICallback {
/**
* 调用b
*
* @param b
*/
void a(B b) {
b.b(this);
}
/**
* 回调函数的实现
*/
@Override
public void back() {
System.out.println("call back A#back()!");
//回调后的执行逻辑...
}
}
class B {
/**
* 回调接口
* <p>
* 注意:回调接口,也可以单独定义一个类,但我觉得还是定义在回调类中更符合逻辑(谁要在我执行的时候被通知,谁就实现我的回调接口)
*/
interface ICallback {
/**
* 回调方法
*/
void back();
}
void b(ICallback callback) {
//...
//B中调用A的回调函数
callback.back();
//...
}
}
从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数back()之外,B类的 b() 方法中的其他逻辑都可以复用。如果 ICallback、B类是框架代码,A类 是使用框架的客户端代码,我们可以通过 ICallback 定制 b() 函数,也就是说,框架因此具有了扩展的能力。
- 应用案例
- JdbcTemplate
Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用 Template(模板)这个单词作为后缀。
详细分析见原稿
异步回调
在函数返回之后执行回调函数
/**
* 异步回调
* <p>
* 在函数返回之后执行回调函数,从应用场景看,异步回调看起来更像观察者模式
*
* @author yiy
* @date 12/29/2021
*/
public class AsyncBackWay {
public static void main(String[] args) {
A1 a = new A1();
B1 b = new B1();
a.a(b);
}
}
class A1 implements B1.ICallback {
void a(B1 b) {
b.b(this);
}
/**
* 回调函数实现
*/
@Override
public void back() {
System.out.println("call back A1#back().");
}
}
class B1 {
/**
* 回调接口
* 注意:回调接口,也可以单独定义一个类,但我觉得还是定义在回调类中更符合逻辑(谁要在我执行的时候被通知,谁就实现我的回调接口)
*/
interface ICallback {
/**
* 回调函数
*/
void back();
}
void b(ICallback callback) {
//b函数的其他逻辑...
//开启线程,执行回调,通知A
new Thread(callback::back).start();
}
}
- 应用案例
- 案例一:setClickListener()
在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件的点击事件注册监听器。
从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { System.out.println("I am clicked."); } });
- 案例一:setClickListener()
我们往 setOnClickListener() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式。
- 案例二:addShutdownHook()
Hook 是 Callback 的一种应用,Callback 更侧重语法机制的描述,Hook 更加侧重应用场景的描述。Hook 比较经典的应用场景是 Tomcat 和 JVM 的 shutdown hook。
模版模式 vs 回调
- 从应用场景上来看:
同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的;
而异步回调跟模板模式有较大差别,更像是观察者模式。
- 从代码实现上来看:
回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
因为组合优于继承,在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:
- 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力;
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类;
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
- 网友理解:
- 个人觉得callback更加灵活,适合算法逻辑较少的场景,实现一两个方法很舒服。比如Guava 的Futures.addCallback 回调 onSuccess onFailure方法。而模板模式适合更加复杂的场景,并且子类可以复用父类提供的方法,根据场景判断是否需要重写更加方便。有个关键的点确实是一个本质的区别:模板基于继承实现子类可以复用父类的方法;而callback基于类的组合关系实现,callback类无法获取到模板类中定义的方法;
- 虽然模板模式和回调很像,甚至和可以互相替换,但是为了让它们的功能和名称更契合,我觉得按照这样原则来使用这两种方法是不是会更好些:如果预留的扩展点必须实现,因为这些扩展点包含和这个类本身相关的关键功能性代码,不实现的话这个类就是个半成品,无法使用,那么使用模板模式,因为模板模式使用的抽象类可以在与语言层面强制这些扩展点必须被实现;如果预留的扩展点可以不实现,或者这些扩展点的实现逻辑甚至可以和这个类完全无关,那么就使用回调,,回调使用的组合关系恰好可以让类和扩展点的实现进行解耦,比如按钮上的事件回调,回调中的逻辑和按钮这个类本身的功能并没有什么关系,甚至回调可以传 null。
不同角度出发,有不同的理解,没有绝对的答案,这就是设计之美么,哈哈,我似乎感受到了!