跳至主要內容

模板模式(上):理解模板模式

AruNi_Lu设计模式设计模式与范式约 2185 字大约 7 分钟

本文内容

前言

本章来讲解另一个比较常用的行为型设计模式 — 模板模式,该模式主要用来解决 复用和扩展 两个问题。

1. 模板模式原理与实现

1.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.

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

定义中的 算法,其实就是 业务逻辑算法骨架 就是 模板算法骨架中的方法 就是 模板方法

1.2 实现

下面是一个简单的模板模式的示例:

public abstract class AbstractClass {
  // final 防止子类重写
  public final void templateMethod() {
    //...
    method1();
    //...
    method2();
    //...
  }
  
  // abstract 强迫子类实现(不是必须的,可灵活设计)
  protected abstract void method1();
  protected abstract void method2();
}

// 子类 1
public class ConcreteClass1 extends AbstractClass {
  @Override
  protected void method1() {
    //...
  }
  
  @Override
  protected void method2() {
    //...
  }
}

// 子类 2
public class ConcreteClass2 extends AbstractClass {
  @Override
  protected void method1() {
    //...
  }
  
  @Override
  protected void method2() {
    //...
  }
}

AbstractClass demo = ConcreteClass1();
demo.templateMethod();

2. 模板模式作用一:复用

先来看模板模式的第一个作用 — 复用模板模式将一个算法中不变的流程抽象到父类的模板方法(templateMethod())中,将可变的部分(method1(), method2())留给子类(ConcreteClass1, ConcreteClass2)实现。所有子类都能复用父类中模板方法定义的流程代码

下面根据 Java 类库中的两个例子来体会一下复用作用。

2.1 Java InputStream

在 Java IO 类库中,很多类都是用到了模板模式,比如 InputStream、OutputStream、Reader、Writer,下面就用 InputStream 来举例说明。

先把 InputStream 跟模板模式相关的代码贴出来:

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;
  }
  
  // abstract 方法
  public abstract int read() throws IOException;
}

// ByteArrayInputStream 子类
public class ByteArrayInputStream extends InputStream {
  //...省略其他代码...
  
  @Override
  public synchronized int read() {
    return (pos < count) ? (buf[pos++] & 0xff) : -1;
  }
}

可以发现,在 InputStream 中,read(......) 是一个模板方法,定义了一个读取数据的整体流程,然后暴露了一个抽象的 read() 方法。

这样 其他子类就可以根据自身的特点只重写 read() 方法,而无需重写一整个读取数据的流程代码了

2.2 Java AbstractList

在 AbstractList 中,有一个 addAll() 方法,它的作用是将一个集合中的数据全部倒入另一个集合(可以从指定 index 开始),定义如下:

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
    boolean modified = false;
    // 遍历集合 c,调用 add() 添加到该集合
    for (E e : c) {
        add(index++, e);
        modified = true;
    }
    return modified;
}

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

可以发现,addAll() 其实就是一个模板方法,add() 是子类需要重写的方法,这样子类就可以复用倒入所有元素的这套流程了。

这里的 add() 方法并没有声明为 abstract,函数实现直接抛出一个异常,所以是很灵活的。

为什么不声明为 abstract?

这里的原因其实和 AQS 抽象类中的模板方法不使用 abstract 修饰?open in new window 类似。

可以想象一下,如果声明为了 abstract,那么子类不管使不使用 add() 方法,都需要实现它(否则声明为抽象类)。是否有点牵强呢?能不能按需实现呢?

因此,这里给出了函数实现,抛出一个异常,这样让子类不必强制实现该方法,如果需要使用,则必须实现,否则会抛出异常,无法正常使用。

3. 模板模式作用二:扩展

模板模式的第二个作用是 扩展:这里的扩展不是指代码的扩展性,而是 框架的扩展性,模板模式可以让框架的用户 在不修改框架的情况下,定制化框架的功能

下面也通过 Java 中的类库和框架,来看看扩展作用。

3.1 Java Servlet

在 Spring 还没有一统 Java 江湖的时候,开发 Web 项目都是使用比较底层的 Servlet 来开发的。我们需要定义一个继承了 HttpServlet 的类,重写其中的 doGet()doPost() 方法:

public class HelloServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
  }
  
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().write("Hello World.");
  }
}

接着需要在 web.xml 配置文件中编写 servlet 配置:

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.xxx.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

这样,Tomcat 容器在启动时,就会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。

当在浏览器输入网址(http://localhost:8080/hello)时,Servlet 容器 Tomcat 会根据收到的请求 URL,找到与之对应的 Servlet(HelloServlet),然后执行它的 service() 方法(service() 方法定义在父类 HttpServlet 中),它会调用 doGet()doPost() 方法,然后输出数据("Hello World.")到网页。

可以发现,核心就是这个 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) {
            doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            if (ifModifiedSince < lastModified) {
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    }
    // ...... 其他方法 ......
}

可以发现,HttpServlet 的 service() 就是一个模板方法,它定义了整个 HTTP 请求的执行流程,doGet()doPost() 是模板中可以由子类来定制的部分。

这就相当于 Servlet 框架提供了一个扩展点(doGet()doPost()),让框架的用户在不修改 Servlet 源码的情况下,将业务代码通过扩展点镶嵌到框架中执行

其实模板模式的核心,就是把固定的东西做成模板,把可变的东西做成扩展点,是一种稳定性与灵活性的平衡。

2.2 JUnit TestCase

JUnit 框架通过模板模式,也提供了一些扩展点(setUp()tearDown() 等),让框架的用户可以在这些扩展点上扩展功能。

在使用 JUnit 框架时,测试类需要继承 TestCase 类,该类中,runBare() 就是模板方法,它定义了执行测试用例的整体流程:

  1. 先执行 setUp(),做一些准备工作;
  2. 执行 runTest(),执行真正的测试代码;
  3. 最后执行 tearDown(),做一些扫尾工作。

TestCase 类的具体实现如下:

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 {
  }
}

可以发现,setUp()tearDown() 不是抽象方法,提供了一个空实现,这样子类就算不实现任意一个方法,都是可以正常执行测试流程的。而需要使用到这两个方法时,子类自己再去实现即可。

4. 总结

模板方法模式就是这么简单,主要就是为了解决 复用和扩展 问题。

复用 很好理解,模板方法中定义的流程,就是一个可复用的代码

扩展 就是 把固定的东西做成模板,把可变的东西做成扩展点,是一种稳定性与灵活性的平衡。

上次编辑于: