模板模式的原理与实现

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

翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的业务逻辑。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
未命名文件.jpg
其中,templateMethod() 函数定义为 final,是为了避免子类重写它。primitiveOperation1() 和 primitiveOperation2() 定义为 abstract,是为了强迫子类去实现。此外,钩子(Hook)是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩由子类自行决定。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现是比较灵活的。

模板模式的作用

1. 复用

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

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

  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. // 具体实现
  30. public abstract int read() throws IOException;
  31. }
  32. public class ByteArrayInputStream extends InputStream {
  33. @Override
  34. public synchronized int read() {
  35. return (pos < count) ? (buf[pos++] & 0xff) : -1;
  36. }
  37. }

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

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

2. 扩展

模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。

Java 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 中配置 URL 和 Servlet 间的映射关系。这样,当我们访问对应 URL 时,Servlet 容器会接收到相应的请求,并根据 URL 和 Servlet 之间的映射关系,找到相应的 Servlet,然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法获取响应数据。

  1. public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  2. HttpServletRequest request;
  3. HttpServletResponse response;
  4. if (!(req instanceof HttpServletRequest &&
  5. res instanceof HttpServletResponse)) {
  6. throw new ServletException("non-HTTP request or response");
  7. }
  8. request = (HttpServletRequest) req;
  9. response = (HttpServletResponse) res;
  10. service(request, response);
  11. }
  12. protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  13. String method = req.getMethod();
  14. if (method.equals(METHOD_GET)) {
  15. // ...
  16. doGet(req, resp);
  17. } else if (method.equals(METHOD_HEAD)) {
  18. // ...
  19. doHead(req, resp);
  20. } else if (method.equals(METHOD_POST)) {
  21. doPost(req, resp);
  22. } else if (method.equals(METHOD_PUT)) {
  23. doPut(req, resp);
  24. } else if (method.equals(METHOD_DELETE)) {
  25. doDelete(req, resp);
  26. } else if (method.equals(METHOD_OPTIONS)) {
  27. doOptions(req,resp);
  28. } else if (method.equals(METHOD_TRACE)) {
  29. doTrace(req,resp);
  30. } else {
  31. String errMsg = lStrings.getString("http.method_not_implemented");
  32. Object[] errArgs = new Object[1];
  33. errArgs[0] = method;
  34. errMsg = MessageFormat.format(errMsg, errArgs);
  35. resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
  36. }
  37. }

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

模板模式 VS 回调

复用和扩展是模板模式的两大作用,实际上,回调(Callback)也能起到跟模板模式相同的作用。相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。在 Java 中通常使用包裹了回调函数的类对象,我们称为回调对象。

从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

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

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

模板模式使用案例

1. Spring Bean 创建

在 Spring Bean 的创建过程中就涉及了模板模式,这也体现了 Spring 的扩展性。利用模板模式,Spring 能让用户定制 Bean 的创建过程。Spring Bean 的创建过程大致可分为两大步:对象的创建和对象的初始化。

对象的创建就是通过反射来动态生成对象,而针对对象的初始化过程,Spring 做了进一步细化,将它拆分成了三个小步骤:初始化前置操作、初始化、初始化后置操作。其中,初始化的前置和后置操作,定义在接口 BeanPostProcessor 中。BeanPostProcessor 的接口定义如下:

  1. public interface BeanPostProcessor {
  2. // 初始化前置操作
  3. Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;
  4. // 初始化后置操作
  5. Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
  6. }

那如何通过 BeanPostProcessor 来定义初始化前置和后置操作呢?我们只需要定义一个实现该接口的处理器类,并在配置文件中像配置普通 Bean 一样去配置即可。Spring 中的 ApplicationContext 会自动检测在配置文件中实现了 BeanPostProcessor 接口的所有 Bean,并把它们注册到 BeanPostProcessor 处理器列表中。在 Spring 容器创建 Bean 的过程中,Spring 会逐一去调用这些处理器。下图展示了 Spring Bean 的整个生命周期:
image.png
不过,这里哪里用到了模板模式啊?模板模式不是需要定义一个包含模板方法的抽象模板类,以及定义子类实现模板方法吗?实际上,这里的模板模式的实现,并不是标准的抽象类的实现方式,而是有点类似 Callback 回调的实现方式,也就是将要执行的函数封装成对象(比如,初始化方法封装成 InitializingBean 对象),传递给模板(BeanFactory)来执行。

2. Java AQS 框架