mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
209 lines
11 KiB
Markdown
209 lines
11 KiB
Markdown
# 模板模式
|
||
|
||
## 定义
|
||
模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:
|
||
> 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() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活,待会儿讲到应用场景的时候,我们会有具体的体现。
|
||
|
||
```
|
||
public abstract class AbstractClass {
|
||
public final void templateMethod() {
|
||
//...
|
||
method1();
|
||
//...
|
||
method2();
|
||
//...
|
||
}
|
||
protected abstract void method1();
|
||
protected abstract void method2();
|
||
}
|
||
|
||
public class ConcreteClass1 extends AbstractClass {
|
||
@Override
|
||
protected void method1() {
|
||
//...
|
||
}
|
||
@Override
|
||
protected void method2() {
|
||
//...
|
||
}
|
||
}
|
||
public class ConcreteClass2 extends AbstractClass {
|
||
@Override
|
||
protected void method1() {
|
||
//...
|
||
}
|
||
@Override
|
||
protected void method2() {
|
||
//...
|
||
}
|
||
}
|
||
AbstractClass demo = ConcreteClass1();
|
||
demo.templateMethod();
|
||
```
|
||
|
||
## 应用场景
|
||
板模式有两大作用:复用和扩展
|
||
|
||
### 复用
|
||
Java InputStream
|
||
Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。
|
||
|
||
把 InputStream 部分相关代码贴在了下面。在代码中,read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了 read(),只是参数跟模板方法不同。
|
||
```
|
||
public abstract class InputStream implements Closeable {
|
||
//...省略其他代码...
|
||
public int read(byte b[], int off, int len) throws IOException {
|
||
if (b == null) {
|
||
throw new NullPointerException();
|
||
} else if (off < 0 || len < 0 || len > b.length - off) {
|
||
throw new IndexOutOfBoundsException();
|
||
} else if (len == 0) {
|
||
return 0;
|
||
}
|
||
int c = read();
|
||
if (c == -1) {
|
||
return -1;
|
||
}
|
||
b[off] = (byte)c;
|
||
int i = 1;
|
||
try {
|
||
for (; i < len ; i++) {
|
||
c = read();
|
||
if (c == -1) {
|
||
break;
|
||
}
|
||
b[off + i] = (byte)c;
|
||
}
|
||
} catch (IOException ee) {
|
||
|
||
}
|
||
return i;
|
||
}
|
||
public abstract int read() throws IOException;
|
||
}
|
||
|
||
public class ByteArrayInputStream extends InputStream {
|
||
//...省略其他代码...
|
||
@Override
|
||
public synchronized int read() {
|
||
return (pos < count) ? (buf[pos++] & 0xff) : -1;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 拓展
|
||
模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转
|
||
基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能
|
||
|
||
JUnit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能
|
||
|
||
在使用 JUnit 测试框架来编写单元测试的时候,我们编写的测试类都要继承框架提供的TestCase 类。在 TestCase 类中,runBare() 函数是模板方法,它定义了执行测试用例的整体流程:先执行 setUp() 做些准备工作,然后执行 runTest() 运行真正的测试代码,最后执行 tearDown() 做扫尾工作。
|
||
|
||
TestCase 类的具体代码如下所示。尽管 setUp()、tearDown() 并不是抽象函数,还提供了默认的实现,不强制子类去重新实现,但这部分也是可以在子类中定制的,所以也符合模板模式的定义。
|
||
|
||
```
|
||
public abstract class TestCase extends Assert implements Test {
|
||
public void runBare() throws Throwable {
|
||
Throwable exception = null;
|
||
setUp();
|
||
try {
|
||
runTest();
|
||
} catch (Throwable running) {
|
||
exception = running;
|
||
} finally {
|
||
try {
|
||
tearDown();
|
||
} catch (Throwable tearingDown) {
|
||
if (exception == null) exception = tearingDown;
|
||
}
|
||
}
|
||
if (exception != null) throw exception;
|
||
}
|
||
/**
|
||
* Sets up the fixture, for example, open a network connection.
|
||
* This method is called before a test is executed.
|
||
*/
|
||
protected void setUp() throws Exception {
|
||
}
|
||
|
||
/**
|
||
* Tears down the fixture, for example, close a network connection.
|
||
* This method is called after a test is executed.
|
||
*/
|
||
protected void tearDown() throws Exception {
|
||
}
|
||
}
|
||
```
|
||
|
||
在 iOS 2.0 开始,App 可以通过重载 UIView 类中的`-(void)drawRect:(CGRect)rect;` 方法来执行定制绘图
|
||
这个方法的默认实现什么也不做,UIView 的子类如果需要真的自己绘制视图,就可以重载这个方法,这个方法也是钩子方法
|
||
当需要改变屏幕上的视图时,这个方法会被调用,框架会处理所有底层的苦差事,以实现这一点。由 UIView 处理绘图过程的部分会调用 drawRect。如果这个方法内有代码,那么会被调用。
|
||
|
||
|
||
## 思考:模板模式与Callback回调函数有何区别和联系
|
||
复用和扩展是模板模式的两大作用,实际上,还有另外一个技术概念,也能起到跟模板模式相同的作用,那就是回调(Callback)。今天我们今天就来看一下,回调的原理、实现和应用,以及它跟模板模式的区别和联系
|
||
|
||
### 回调的原理解析
|
||
相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。
|
||
|
||
A 类如何将回调函数传递给 B 类呢?不同的编程语言,有不同的实现方法。C 语言可以使用函数指针,Java 则需要使用包裹了回调函数的类对象,我们简称为回调对象。这里用Java 语言举例说明一下。代码如下所示:
|
||
```
|
||
public interface ICallback {
|
||
void methodToCallback();
|
||
}
|
||
public class BClass {
|
||
public void process(ICallback callback) {
|
||
//...
|
||
callback.methodToCallback();
|
||
//...
|
||
}
|
||
}
|
||
public class AClass {
|
||
public static void main(String[] args) {
|
||
BClass b = new BClass();
|
||
b.process(new ICallback() { //回调对象
|
||
@Override
|
||
public void methodToCallback() {
|
||
System.out.println("Call back me.");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
上面就是 Java 语言中回调的典型代码实现。从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass 类的 process() 函数中的逻辑都可以复用。如果 ICallback、BClass 类是框架代码,AClass 是使用框架的客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说,框架因此具有了扩展的能力
|
||
|
||
实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
|
||
|
||
回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式
|
||
|
||
实际场景的例子
|
||
在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件的点击事件注册监听器。
|
||
```
|
||
Button button = (Button)findViewById(R.id.button);
|
||
button.setOnClickListener(new OnClickListener() {
|
||
@Override
|
||
public void onClick(View v) {
|
||
System.out.println("I am clicked.");
|
||
}
|
||
});
|
||
```
|
||
从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。
|
||
|
||
我们前面讲到,回调分为同步回调和异步回调。这里的回调算是异步回调,我们往 setOnClickListener() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式
|
||
|
||
## 总结
|
||
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来
|
||
|
||
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能
|
||
|