设计模式学习笔记 - 设计模式与范式 -行为型:3.模板模式(上):剖析模板模式在JDK、Servlet、Junit中的应用

概述

在这里插入图片描述

本章学习模板模式。设计模式的原理和实现都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式主要是用来解决复用扩展两个问题。


模板模式的原理与实现

模板模式,全称是模板方法模式,英文是 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() 留给子类 ConcreteClass1ConcreteClass2 来实现。所有的子类都可以复用父类中模板方法中定义的流程代码。我们通过两个小例子来更加直观地体会一下。

1.Java InputStream

Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStreamOutputStreamReaderWriter 。我们用 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 的类,并重写其中的 doGetdoPost() 方法,来分别处理 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,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

模版模式有两大作用:复用和扩展。

  • 复用指的是,所有的子类可以复用父类中提供的模板方法的代码。
  • 扩展指的是,框架通过模板模式提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值