【Design Pattern】Structural - Template Method

Posted by 西维蜀黍 on 2018-11-12, Last Modified on 2023-08-23

1 动机

2 定义

在阎宏博士的《Java与模式》一书中开头是这样描述模板方法(Template Method)模式的:

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

  • 模板方法模式是基于继承的代码复用基本思想,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中。
  • 在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

3 构成

UML图

构成

  • 抽象模板类(AbstractClass):
    • 定义了抽象的原语方法(primitive method) ,以便让子类具体来实现这些原语方法;
    • 具体实现一个模板方法(template method),在这个模板方法中定义了对应算法的骨架。该模板方法调用所有的原语方法;
  • 具体模板类 (ConcreteClass): 实现父类中定义的抽象原语方法,以完成算法中与特定子类相关的步骤。

示范

抽象模板角色类

抽象模板角色类,abstractMethod()hookMethod() 等基本方法是顶级逻辑的组成步骤,这个顶级逻辑由 templateMethod() 方法代表。

public abstract class AbstractTemplate {
    /**
     * 模板方法
     */
    public void templateMethod(){
        //调用基本方法
        abstractMethod();
        hookMethod();
        concreteMethod();
    }
    /**
     * 基本方法的声明(由子类实现)
     */
    protected abstract void abstractMethod();
    /**
     * 基本方法(空方法)
     */
    protected void hookMethod(){}
    /**
     * 基本方法(已经实现)
     */
    private final void concreteMethod(){
        //业务相关的代码
    }
}

具体模板角色类

具体模板角色类,实现了父类所声明的基本方法,abstractMethod()方法所代表的就是强制子类实现的剩余逻辑,而hookMethod()方法是可选择实现的逻辑,不是必须实现的。

public class ConcreteTemplate extends AbstractTemplate{
    //基本方法的实现
    @Override
    public void abstractMethod() {
        //业务相关的代码
    }
    //重写父类的方法
    @Override
    public void hookMethod() {
        //业务相关的代码
    }
}

4 示例

背景

假设我们作为一个建筑公司,需要设计一个方案来实现建造不同的房子,我们可能会建造木房子或玻璃房子。

我们可以定义一个抽象类HouseTemplate表示建造任何类型房子,木房子建造类WoodenHouse和玻璃房子建造类GlassHouse均实现了HouseTemplate抽象类。

建造房子的步骤包括:

  • 修建地基(buildFoundation()
  • 修建柱子(buildPillars()
  • 修建墙面(buildWalls()
  • 安装窗(buildWindows()

Note:以上每个步骤都对应一个特定的方法。

显然,我们不能任意颠倒以上步骤的顺序(比如,在修建地基之前就安装窗)。因此,我们通过定义一个buildHouse()模板方法(template method)来指定建造步骤的执行顺序。

Note:buildHouse()方法不能被子类改写(因为,即使是建造不同类型的房子,建房的四大核心步骤的顺序也是相同的)

同时,无论建造什么房子(木房子或玻璃房子),我们打地基的方式可能都是一样的。所以,我们可以在抽象类HouseTemplate中,用最常用的打地基方式来实现buildFoundation()。如果在建造一个特殊类型的房子时,需要用一个特殊的方法来打地基。则可以在房子子类中重写(override)buildFoundation()方法。


假设我们认为对于不同类型的房子,修建柱子修建墙面是没有统一的方法的。我们将buildPillars()buildWalls()定义为抽象方法(abstract),以让子类显式地去实现。

实现

HouseTemplate 房子建造抽象类

public abstract class HouseTemplate {

    //template method defining the order of execution for performing several steps, final so subclasses can't override
    public final void buildHouse(){
        buildFoundation();
        buildPillars();
        buildWalls();
        buildWindows();
        System.out.println("House is built.");
    }

    public void buildWindows() {
        System.out.println("Building Glass Windows");
    }
    //default implementation
    public void buildFoundation() {
        System.out.println("Building foundation with cement,iron rods and sand");
    }

    //methods to be implemented by subclasses
    public abstract void buildWalls();
    public abstract void buildPillars();

}

WoodenHouse 木房子建造类

public class WoodenHouse extends HouseTemplate {

    @Override
    public void buildWalls() {
        System.out.println("Building Wooden Walls");
    }

    @Override
    public void buildPillars() {
        System.out.println("Building Pillars with Wood coating");
    }

}

GlassHouse玻璃房子建造类

public class GlassHouse extends HouseTemplate {

    @Override
    public void buildWalls() {
        System.out.println("Building Glass Walls");
    }

    @Override
    public void buildPillars() {
        System.out.println("Building Pillars with glass coating");
    }

}

调用

public class HousingClient {
    public static void main(String[] args) {
        HouseTemplate houseType = new WoodenHouse();
        
        //using template method
        houseType.buildHouse();
        System.out.println("************");
        
        houseType = new GlassHouse();
        houseType.buildHouse();
    }
}

问题引入

如果有一天,有一个客户希望他的房子是没有窗的,那怎么办呢?

我们可以从容地在HouseTemplate抽象类中,添加一个hook 方法hasWindows(),且默认返回true。且简单修改一下模板方法:

public boolean hasWindows(){
    return true;
}

//template method 
public final void buildHouse(){
    buildFoundation();
    buildPillars();
    buildWalls();
    
    if (hasWindows()){
        buildWindows();
    }
    System.out.println("House is built.");
}

当不需要窗时,只需要在子类中重写hasWindows()方法,并将返回值修改为false即可。

5 适用场景

  • 模板方法中定义一个操作或行为的内部逻辑执行顺序,这个操作无论在什么样的执行环境中执行,其内部执行顺序均不会改变
  • 抽象类中定义了一些操作的默认实现(显然,子类可以在需要的时候重写这些操作)

6 模式应用

模板方法模式在Servlet中的应用

使用过Servlet的人都清楚,除了要在web.xml做相应的配置外,还需继承一个叫HttpServlet的抽象类。

HttpService类提供了一个service()方法,这个方法调用七个do方法中的一个或几个,完成对客户端调用的响应。

这些do方法需要由HttpServlet的具体子类提供,这是典型的模板方法模式。

下面是service()方法的源代码:

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 / 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);
    }
}

当然,这个service()方法也可以被子类置换掉。

下面给出一个简单的Servlet例子:

从上面的类图可以看出,TestServlet类是HttpServlet类的子类,并且置换掉了父类的两个方法:doGet()和doPost()。

public class TestServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        System.out.println("using the GET method");

    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
            
        System.out.println("using the POST method");
    }

}

分析

从上面的例子可以看出这是一个典型的模板方法模式。

  • HttpServlet担任抽象模板角色

    • 模板方法:由service()方法担任。
    • 基本方法:由doPost()、doGet()等方法担任。
  • TestServlet担任具体模板角色

TestServlet置换掉了父类HttpServlet中七个基本方法中的其中两个,分别是doGet()和doPost()。

Reference