image.png

概念感受——模板

模板,原义是指带有镂空文字的薄薄塑料板。只要用笔在模板的镂空处进行临摹,即使是手写也能写出整齐的文字。虽然只要看到这些镂空的洞,我们就可以知道能写出哪些文字,但是具体写出的文字是什么感觉则依赖于所用的笔。如果使用签字笔来临摹,则可以写出签字似的文字;如果使用铅笔来临摹,则可以写出铅笔字;而如果是用彩色笔临摹,则可以写出彩色的字。但是无论使用什么笔,文字的形状都会与模板上镂空处的形状一致。

模板方法设计模式 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.——GoF
在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

这里的「算法」可理解为广义上的「业务逻辑」。模板方法模式在父类中定义处理流程的框架,允许子类中实现每个流程的具体处理。

登场角色:

  • AbstractClass(抽象类):实现模板方法,声明在模板方法中所使用到的抽象方法。这些方法由子类 ConcreteClass 角色负责实现。
  • ConcreteClass(具体类):实现 AbstractClass 角色中定义的抽象方法。这里的实现方法会在 AbstractClass 角色得模板方法中被调用。

image.png

在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

观察上面的 AbstractClass,其中 templateMethod 为模板方法,里面使用了 method1method2method3 抽象方法。这些抽象方法需要子类去实现。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TemplateTest {
public static void main(String[] args) {
AbstractClass c = new ConcreteClass();
c.templateMethod();
}
}

abstract class AbstractClass{
abstract public void method1();
abstract public void method2();
abstract public void method3();

final public void templateMethod(){
method1();
method2();
method3();
}
}

class ConcreteClass extends AbstractClass{

@Override
public void method1() {
System.out.println("1. 打开大门");
}

@Override
public void method2() {
System.out.println("2. 装入大象");
}

@Override
public void method3() {
System.out.println("3. 关上大门");
}
}

类的层次与抽象类:

  • 父类对子类的要求(子类责任):父类期待子类去实现抽象方法;父类要求子类去实现抽象方法。
  • 抽象类的意义:在抽象类阶段确定处理的流程。
  • 父类子类协作:降低子类灵活性,子类可能会臃肿。

拓展思路要点:

  • 父类的模板方法中编写了算法,因此无需在每个子类中再编写算法。模板方法出现 Bug 时,只需改模板方法。
  • 父类子类紧密联系。在子类中实现父类中声明的抽象方法时,必须要理解抽象方法被调用的时机。
  • 父类与子类的一致性,里氏替换原则 LSP。使用父类类型的变量保存子类实例的优点是,即使没有用 instanceof 等指定子类的种类,程序也能正常工作。LSP 是通用的继承原则,并非仅限于模板方法模式。

两大作用:

  • 复用:所有的子类都可以复用父类中模板方法定义的流程代码。
  • 扩展:这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性。模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。比如,在模板方法中,会调用一些抽象方法,而这些方法的具体实现可以交由子类去扩展。

应用场景:

  1. 算法的整体框架已经确定,但某些具体步骤的实现可能会有所不同。模板方法模式可以在抽象类中定义整体的算法框架,而将具体步骤的实现留给子类来完成。
  2. 多个类具有相似的算法结构,但其中的某些步骤可能有所不同。通过使用模板方法模式,可以将这些公共的代码逻辑提取到抽象类中的模板方法中,避免代码的重复。
  3. 需要在不破坏原有算法框架的情况下,对算法中的某些步骤进行扩展或修改。通过使用模板方法模式,可以在抽象类中定义通用的算法流程,而在子类中重写需要修改或扩展的具体步骤。
  4. 需要在多个相关类中实现一些公共行为,而不希望将这些行为放在一个单独的类中管理。模板方法模式可以将这些公共行为封装在抽象类中的模板方法中,使得不同的类可以根据需要继承该抽象类并实现具体的步骤。
  5. 需要控制算法的执行顺序和流程。通过模板方法模式,可以在抽象类中控制算法的整体结构,确保每个步骤按照预期顺序执行。

相关的设计模式

站内文章工厂(方法)模式 是将模板方法模式用于生成实例的一个典型例子。

与策略模式的对比:

设计模式 模板方法模式 策略模式
改变程序行为的技术 继承。在父类中定义程序行为的框架,在子类中决定具体的处理。 委托。
关注点 算法的整体框架、步骤。 不同的算法实现。将算法的实现与具体的类解耦。
改变程序行为 子类可以改变部分程序行为。 替换整个算法。

Java 源代码中的模板方法模式

InputStreamOutputStreamReaderWriter 类都使用了模板方法模式。AbstractListaddAll() 函数也使用了模板方法模式。

Java 中的 Servlet 生命周期就是使用了模板方法模式,其中抽象类 HttpServlet 定义了 doGetdoPost 等操作方法,而具体的 Servlet 类可以继承 HttpServlet 并重写具体的操作方法来实现自定义的业务逻辑。

回调函数

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是「回调函数」。A 调用 B,B 反过来又调用 A,这种调用机制就叫作「回调」。

对于回调,不同的编程语言有不同的实现方式,C 语言中使用函数指针来实现。

回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。

同步回调

Java 中同步回调函数的典型实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface ICallback {
void methodToCallback();
}

// B 类内置类处理回调函数的方法
public class BClass {
public void process(ICallback callback) {
//...
callback.methodToCallback();
//...
}
}

// A 类定义了具体回调函数的逻辑
public class AClass {
public static void main(String[] args) {
BClass b = new BClass();
// A类中定义回调对象,并将其交给B类
b.process(new ICallback() {
@Override
public void methodToCallback() {
System.out.println("Call back me.");
}
});
}
}

同步回调更像是模板方法模式:将不变的执行流程抽离出来,放到模板方法中,然后将可变的部分实际成回调,由用户来定制。

异步回调

比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

回调与模板模式

从应用场景上来看,同步回调看起来更像模板模式,它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。异步回调看起来更像观察者模式。

从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

组合优于继承。在代码实现上,回调相对于模板模式会更加灵活,主要体现在:

  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

回调与钩子

Callback 是一种异步调用的实现,Callback 本意就是你传递一个函数给对方,当对方的工作有进展的时候就调用这个函数通知你。

Hook 则是一种 API 拦截手段,特点就是不改变原有双边的逻辑的情况下,在 API 接口上插入一个拦截调用的 Hook 函数,从而截取调用数据、甚至可以改变程序行为。

Java 8 之后的模板方法模式新写法

Java 8 中最大的新特性就是提供了对 站内文章函数式编程 的支持。Java 8 在 java.util.function 下面增加增加一系列的函数接口。其中主要有 ConsumerSupplierPredicateFunction 等。

在上文中,我们可以知道模板方法模式中,子类通过实现抽象方法替换父类算法框架中的某个步骤。但是如果父类的模板方法中,一些步骤可以进行多种替换。如果仅靠子类实现将会需要编写许多的类。

我们可以进行以下改进:

  • 使用函数式编程,让算法步骤作为一个函数类型传入到模板方法中
  • 在统一的类中定义不同的算法实现,免去继承的麻烦

文章开头的案例可以改造为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TemplateTest {
public static void main(String[] args) {
MyHandler c = new MyHandler();
c.putElephantIntoRefrigerator();
}
}

class MyHandler {
/**
* 这是模板方法
* @param step1 步骤一
* @param step2 步骤二
* @param step3 步骤三
*/
final public void templateMethod(Runnable step1, Runnable step2, Runnable step3){
step1.run();
step2.run();
step3.run();
}

public void putElephantIntoRefrigerator(){
templateMethod(
()->System.out.println("1. 打开大门"),
()->System.out.println("2. 装入大象"),
()->System.out.println("3. 关上大门")
);
}

/* 此处可以扩展更多的方法 */
}

长得很像上一节讲的同步回调。

本文 PlantUML 归档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class AbstractClass{
{method} {abstract} method1
{method} {abstract} method2
{method} {abstract} method3
{method} templateMethod
}

class ConcreteClass{
{method} method1
{method} method2
{method} method3
}

AbstractClass <|-- ConcreteClass

note right of AbstractClass::templateMethod
方法使用final关键字修饰,
表明不应该重写模板方法
end note

本文参考