概述
本章学习模板模式。设计模式的原理和实现都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式主要是用来解决复用和扩展两个问题。
模板模式的原理与实现
模板模式,全称是模板方法模式,英文是 Template Method Design Pattern。在 GoF 的《设计模式》中,定义为:
Define the skeleton of an algorithm in an operation, deferring some steps to subclass. Template Method lets subclass 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() {
// ...
}
}
模板模式作用一:复用
开篇时我们讲到模板模式有两大作用:复用和扩展。先来看它的第一个作用:复用。
模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod()
中,将可变的部分 method1()
、method2()
留给子类 ConcreteClass1
、ConcreteClass2
来实现。所有的子类都可以复用父类中模板方法中定义的流程代码。我们通过两个小例子来更加直观地体会一下。
1.Java InputStream
Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream
、OutputStream
、Reader
、Writer
。我们用 InputStream
来举例说明下。
InputStream
部分相关代码贴在了下面。在代码中 read()
函数就是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个可定制的方法也被命名为了 read()
,只是参数跟模板方法不同。
public abstract class InputStream implements Closeable {
// 省略其他代码...
public abstract int read() throws IOException;
// ...
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 class ByteArrayInputStream extends InputStream {
// 省略其他代码...
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
// ...
}
2.Java AbstractList
在 Java AbstractList
类中,addAll()
函数可以看做模板方法,add()
是子类需要重写的方法,尽管没有声明为 abstract 的,但是函数实现直接抛出了 UnsupportedOperationException
异常。前提是,如果子类不重写是不能使用的。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// ...
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
// ...
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
// ...
}
模板模式作用而:扩展
模板模式的第二大作用是扩展。这里所说的扩展,并不是指代码的扩展性,而是框架的扩展性,有点类似之前讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。下面通过 JUint TestCase、Java Servlet 两个例子来解释下。
1.Java Servlet
对 Java Web 项目开发来说,常用的开发框架是 SpringMVC。利用它,只需要关注业务代码的编写,底层的原理几乎不会涉及。但是,如果抛开这些高级框架开发 Web 项目,必然会用到 Servlet。实际上,使用比较底层的 Servlet 来开发 Web 项目也不难。我们只需要定义一个继承 HttpServlet
的类,并重写其中的 doGet
或 doPost()
方法,来分别处理 get 和 post 请求。具体代码如下所示:
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
中做如下配置。Tomcat、Jetty 等 Servlet 容器在启动的时候,会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</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;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
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;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// 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 {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
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 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。
2.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;
}
// ...
protected void setUp() throws Exception {
}
protected void tearDown() throws Exception {
}
// ...
}
总结
模板方法模式在一个方法中定义算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以在子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
- 这里的 “算法”,我们可以理解为广义上的 “业务逻辑”,不是特指数据结构和算法中的 “算法”。
- 这里的算法骨架就是 “模版”,包含算法骨架的方法就是 “模板方法”。
在模板模式的经典实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
模版模式有两大作用:复用和扩展。
- 复用指的是,所有的子类可以复用父类中提供的模板方法的代码。
- 扩展指的是,框架通过模板模式提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架代码。