Files
knowledge-kit/Chapter6 - Design Pattern/6.21.md
2023-10-31 15:17:07 +08:00

209 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模板模式
## 定义
模板模式,全称是模板方法设计模式,英文是 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 algorithms 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 调用 BB 反过来又调用 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() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式
## 总结
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能