模板模式(上):理解模板模式
本文内容
前言
本章来讲解另一个比较常用的行为型设计模式 — 模板模式,该模式主要用来解决 复用和扩展 两个问题。
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 修饰? 类似。
可以想象一下,如果声明为了 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()
就是模板方法,它定义了执行测试用例的整体流程:
- 先执行
setUp()
,做一些准备工作; - 执行
runTest()
,执行真正的测试代码; - 最后执行
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. 总结
模板方法模式就是这么简单,主要就是为了解决 复用和扩展 问题。
复用 很好理解,模板方法中定义的流程,就是一个可复用的代码。
而 扩展 就是 把固定的东西做成模板,把可变的东西做成扩展点,是一种稳定性与灵活性的平衡。