Skip to content

Latest commit

 

History

History
208 lines (177 loc) · 10.8 KB

File metadata and controls

208 lines (177 loc) · 10.8 KB

模板模式

定义

模板模式,全称是模板方法设计模式,英文是 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() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式

总结

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来

模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能