设计模式 - 模板方法模式详解

介绍&定义

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

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

这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

结构

  • AbstractClass:抽象类。用来定义算法框架和抽象操作,具体的子类通过重定义这些抽象操作来实现一个算法的各个步骤。在这个类里面,还可以提供算法中通用的实现。
  • ConcreteClass:具体实现类。用来实现算法框架中的某些步骤,完成与特定子类相关的功能。

实现案例

image-20230217141250997


public class TemplateMethodPattern {

    public static void main(String[] args) {
        AbstractBankBusinessTemplateMethod templateMethodA = new ConcreteBodyA();
        templateMethodA.doWork();
        System.out.println("===================================================");
        AbstractBankBusinessTemplateMethod templateMethodB = new ConcreteBodyB();
        templateMethodB.doWork();
        System.out.println("===================================================");
        AbstractBankBusinessTemplateMethod templateMethodC = new ConcreteBodyC();
        templateMethodC.doWork();
    }
}

//取号、排队、办理具体业务、对银行工作人员进行评分
abstract class AbstractBankBusinessTemplateMethod{

    private void getNo(){
        System.out.println("取号");
    }
    private void lineUp(){
        System.out.println("排队");
    }

    abstract void deal();

    private void evaluate(){
        System.out.println("评价");
    }

    public final void doWork(){
        getNo();
        lineUp();
        deal();
        evaluate();
    }

}

class ConcreteBodyA extends AbstractBankBusinessTemplateMethod{

    @Override
    public void deal() {
        System.out.println("======取款=======");
    }
}
class ConcreteBodyB extends AbstractBankBusinessTemplateMethod{

    @Override
    public void deal() {
        System.out.println("======存款======");
    }
}
class ConcreteBodyC extends AbstractBankBusinessTemplateMethod{

    @Override
    public void deal() {
        System.out.println("======贷款======");
    }
}
取号
排队
======取款=======
评价
===================================================
取号
排队
======存款======
评价
===================================================
取号
排队
======贷款======
评价

特点

复用

模板模式有两大作用:复用和扩展。我们先来看它的第一个作用:复用。

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

Java InputStream

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

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

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;
    }
    public  abstract  int  read()  throws IOException;
}
public  class  ByteArrayInputStream  extends  InputStream {
    //...省略其他代码...
    @Override  
    public  synchronized  int  read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
}
扩展

这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转,你可以结合第 19 节来一块理解。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。我们通过 Junit TestCase、Java Servlet 两个例子来解释一下。

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.xzg.cd.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;
    
    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) {
        // servlet doesn't support if-modified-since, no reason
        // to go through further expensive logic
        doGet(req, resp);
    
    } else {
    
        long  ifModifiedSince  = req.getDateHeader(HEADER_IFMODSINCE);
    
        if (ifModifiedSince < lastModified) {
            
            // 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 {
    
        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 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

模板模式与Callback回调函数有何区别和联系

回调原理解析

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。

A 类如何将回调函数传递给 B 类呢?不同的编程语言,有不同的实现方法。C 语言可以使用函数指针,Java 则需要使用包裹了回调函数的类对象,我们简称为回调对象。这里我用 Java 语言举例说明一下。代码如下所示:

public  interface ICallback {
    void  methodToCallback();
}
public  class  BClass {
    public  void  process(ICallback callback) {
        //...
        callback.methodToCallback();
        //...
    }
}
public  class  AClass {
    public  static  void  main(String[] args) {
        BClass b = new  BClass();
        b.process(new  ICallback() { //回调对象
            @Override  
            public  void  methodToCallback() {
                System.out.println("Call back me.");
            }
        });
    }
}

上面就是 Java 语言中回调的典型代码实现。从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass 类的 process() 函数中的逻辑都可以复用。如果 ICallback、BClass 类是框架代码,AClass 是使用框架的客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说,框架因此具有了扩展的能力。

实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

模板模式 VS 回调

从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。

从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

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

像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。

回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。

如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

参考文章

极客时间-设计模式之美

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值