今天,我们再学习另外一种行为型设计模式,模板模式。我们多次强调,绝大部分设计模式的原理和实现,都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式也不例外。

1、什么是模板方法模式?

模板模式,全称是模板方法设计模式,英文是 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.

翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

2、为什么要使用模板方法模式?

模板模式主要是用来解决复用和扩展两个问题。

将通用算法抽象出来,延迟到子类具体实现。

如果一些方法通用,却在每一个子类都重新写了这一方法,可以使用模板方法模式来重构。

3、例子

3.1、GoF(简单)

image.png

  1. package template.version2;
  2. public class Main {
  3. public static void main(String[] args) {
  4. AbstractClass c;
  5. c = new ConcreteClassA();
  6. c.TemplateMethod();
  7. c = new ConcreteClassB();
  8. c.TemplateMethod();
  9. }
  10. }
  11. /**
  12. * AbstractClass 是抽象类,其实也就是一抽象模板,定义并实现了
  13. * 一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级
  14. * 逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实
  15. * 现。顶级逻辑也有可能调用一些具体方法。
  16. */
  17. abstract class AbstractClass {
  18. public abstract void method1();
  19. public abstract void method2();
  20. public void TemplateMethod() {
  21. method1();
  22. method2();
  23. }
  24. }
  25. /**
  26. * ConcreteClass, 实现父类所定义的一个或多个抽象方法。每一
  27. * 个AbstractClass 都可以有任意多个ConcreteClass 与之对应,而每一
  28. * 个ConcreteClass 都可以给出这些抽象方法(也就是顶级逻辑的组成步
  29. * 骤)的不同实现,从而使得顶级逻辑的实现各不相同。
  30. */
  31. class ConcreteClassA extends AbstractClass {
  32. @Override
  33. public void method1() {
  34. System.out.println("ConcreteClassA:method1");
  35. }
  36. @Override
  37. public void method2() {
  38. System.out.println("ConcreteClassA:method2");
  39. }
  40. }
  41. class ConcreteClassB extends AbstractClass {
  42. @Override
  43. public void method1() {
  44. System.out.println("ConcreteClassB:method1");
  45. }
  46. @Override
  47. public void method2() {
  48. System.out.println("ConcreteClassB:method2");
  49. }
  50. }

image.png

我们先来看它的第一个作用:复用。模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ConcreteClassA 和 ConcreteClassB 来实现。所有的子类都可以复用父类中模板方法定义的流程代码。我们通过下面的两个小例子来更直观地体会一下。

3.2、Java InputStream(中等)复用

Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。

我把 InputStream 部分相关代码贴在了下面。在代码中,read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了 read(),只是参数跟模板方法不同。

  1. public abstract class InputStream implements Closeable {
  2. //...省略其他代码...
  3. public int read(byte b[], int off, int len) throws IOException {
  4. if (b == null) {
  5. throw new NullPointerException();
  6. } else if (off < 0 || len < 0 || len > b.length - off) {
  7. throw new IndexOutOfBoundsException();
  8. } else if (len == 0) {
  9. return 0;
  10. }
  11. int c = read();
  12. if (c == -1) {
  13. return -1;
  14. }
  15. b[off] = (byte)c;
  16. int i = 1;
  17. try {
  18. for (; i < len ; i++) {
  19. c = read();
  20. if (c == -1) {
  21. break;
  22. }
  23. b[off + i] = (byte)c;
  24. }
  25. } catch (IOException ee) {
  26. }
  27. return i;
  28. }
  29. public abstract int read() throws IOException;
  30. }
  31. public class ByteArrayInputStream extends InputStream {
  32. //...省略其他代码...
  33. @Override
  34. public synchronized int read() {
  35. return (pos < count) ? (buf[pos++] & 0xff) : -1;
  36. }
  37. }

3.3、Java AbstractList(中等)复用

在 Java AbstractList 类中,addAll() 函数可以看作模板方法,add() 是子类需要重写的方法,尽管没有声明为 abstract 的,但函数实现直接抛出了 UnsupportedOperationException 异常。前提是,如果子类不重写是不能使用的。

  1. public boolean addAll(int index, Collection<? extends E> c) {
  2. rangeCheckForAdd(index);
  3. boolean modified = false;
  4. for (E e : c) {
  5. add(index++, e);
  6. modified = true;
  7. }
  8. return modified;
  9. }
  10. public void add(int index, E element) {
  11. throw new UnsupportedOperationException();
  12. }

模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。我们通过 Junit TestCase、Java Servlet 两个例子来解释一下。

3.4、Java Servlet(中等)扩展

对于 Java Web 项目开发来说,常用的开发框架是 SpringMVC。利用它,我们只需要关注业务代码的编写,底层的原理几乎不会涉及。但是,如果我们抛开这些高级框架来开发 Web 项目,必然会用到 Servlet。实际上,使用比较底层的 Servlet 来开发 Web 项目也不难。我们只需要定义一个继承 HttpServlet 的类,并且重写其中的 doGet() 或 doPost() 方法,来分别处理 get 和 post 请求。具体的代码示例如下所示:

  1. public class HelloServlet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. this.doPost(req, resp);
  5. }
  6. @Override
  7. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  8. resp.getWriter().write("Hello World.");
  9. }
  10. }

除此之外,我们还需要在配置文件 web.xml 中做如下配置。Tomcat、Jetty 等 Servlet 容器在启动的时候,会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。

  1. <servlet>
  2. <servlet-name>HelloServlet</servlet-name>
  3. <servlet-class>com.xzg.cd.HelloServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>HelloServlet</servlet-name>
  7. <url-pattern>/hello</url-pattern>
  8. </servlet-mapping>

当我们在浏览器中输入网址(比如,http://127.0.0.1:8080/hello )的时候,Servlet 容器会接收到相应的请求,并且根据 URL 和 Servlet 之间的映射关系,找到相应的 Servlet(HelloServlet),然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,然后输出数据(“Hello world”)到网页。

我们现在来看,HttpServlet 的 service() 函数长什么样子。

public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException
{
    HttpServletRequest  request;
    HttpServletResponse response;
    if (!(req instanceof HttpServletRequest &&
            res instanceof HttpServletResponse)) {
        throw new ServletException("non-HTTP request or response");
    }
    request = (HttpServletRequest) req;
    response = (HttpServletResponse) res;
    service(request, response);
}

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            if (ifModifiedSince < lastModified) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }
    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);
    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

从上面的代码中我们可以看出,HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。实际上,这就相当于 Servlet 框架提供了一个扩展点(doGet()、doPost() 方法),让框架用户在不用修改 Servlet 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

3.5、Junit TestCase(中等)扩展

跟 Java Servlet 类似,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 {
  }
}

4、总结

4.1、优缺点

1)优点

它常用在框架开发中,通过提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架的功能。除此之外,模板模式还可以起到代码复用的作用;

2)缺点

每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

4.2、模板方法模式与 Callback 回调函数有何区别和联系?

我们从应用场景和代码实现两个角度,来对比一下模板模式和回调。

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

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

前面我们也讲到,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。

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

4.3、关于模板方法模式的一些问题