模板方法模式 Template Method

559 阅读8分钟

模板方法

模板方法模式(Template Method),定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

The Template Method pattern is a behavioral design pattern that defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It allows subclasses to redefine certain steps of the algorithm without changing its structure.

模板方法模式应该是最简单的行为型设计模式了,但还是有一些小技巧的,下面先看一下UML类图:

image.png

模版方法模式角色如下:

AbstractClass(抽象类):定义了一个模板方法(Template Method),其中包含了算法的框架及具体步骤的声明,部分步骤可以是抽象方法,由具体子类实现。

ConcreteClass(具体子类) :继承自抽象类,并实现了其中定义的抽象方法,完成算法中具体步骤的实现。

UML 类图结构很简单,就一个继承关系,模板方法模式的重点在 AbstractClass(抽象类)上,AbstractClass 由一个模板方法和若干个基本方法构成:

模板方法:定义了一套算法的骨架,按某种顺序调用其包含的基本方法。

基本方法:是算法骨架/流程的某些步骤进行具体实现,包含以下几种类型:

▪ ▪ 抽象方法:在抽象类中声明,必须由具体子类实现。

▪ ▪ 具体方法:在抽象类中已经实现,具体子类中可以选择重写它。

▪ ▪ 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

基本实现

抽象类 Abstract Class

AbstractClass 是抽象类,其实也就是一抽象模板,定义并实现了一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。

public abstract class AbstractClass {

    // 模板方法
    public final void templateMethod() {
        abstractMethod();  // 步骤1
        concreteMethod();  // 步骤2
        emptyMethod();     // 步骤3
        if(judgeMethod()){ // 步骤4
            System.out.println("任务完成");
        }
    }

    // 抽象方法,子类必须实现
    protected abstract void abstractMethod();

    // 具体方法,子类选择性实现
    protected void concreteMethod(){
        System.out.println("具体方法完成步骤2");
    }

    // 钩子方法(空方法),子类选择性实现
    protected void emptyMethod(){

    }

    // 钩子方法(判断方法),子类选择性实现
    protected boolean judgeMethod(){
        return false;
    }

}

具体子类 ConcreteClass

ConcreteClass,实现父类所定义的一个或多个抽象方法。每一个 AbstractClass 都可以有任意多个 ConcreteClass 与之对应,而每一个 ConcreteClass 都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。

▪ 具体子类A

具体子类A给出了抽象方法的具体实现,并且重写判断钩子方法:

public class ConcreteClassA extends AbstractClass{

    @Override
    protected void abstractMethod() {
        System.out.println("ConcreteClassA 完成步骤1");
    }

    @Override
    protected boolean judgeMethod(){
        return true;
    }

}

▪ 具体子类B

具体子类B给出了抽象方法的具体实现,并且重写空实现的钩子方法:

public class ConcreteClassB extends AbstractClass{

    @Override
    protected void abstractMethod() {
        System.out.println("ConcreteClassB 完成步骤1");
    }

    @Override
    protected void emptyMethod(){
        System.out.println("ConcreteClassB 完成步骤3");
    }

}

客户端 Client

AbstractClass template = new ConcreteClassA();
template.templateMethod();

System.out.println("*****************");

template = new ConcreteClassB();
template.templateMethod();

输出结果如下:

ConcreteClassA 完成步骤1
具体方法完成步骤2
任务完成
*****************
ConcreteClassB 完成步骤1
具体方法完成步骤2
ConcreteClassB 完成步骤3

ConcreteClassAConcreteClassB 的算法骨架相同,但具体个别步骤不同。

这里需要注意的是,钩子方法可以是空方法,可以是判断方法,通过钩子方法的重写可以将模板方法(templateMethod)中的某个步骤移除或加入。

源码赏析

JDK 之 AbstractList

java.util.AbstractList 是一个抽象类,它实现了 List 接口的绝大部分方法,利用这些实现,子类只需专注于实现核心的抽象方法,如 addAll 就可以看作是模板方法

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

其中,rangeCheckForAdd 方法用于检查在添加元素时索引是否在有效范围内,它是一个具体方法,子类可以根据自己的需要选择覆盖:

private void rangeCheckForAdd(int index) {
    if (index < 0 || index > size())
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

add 方法也是一个具体方法,子类可以根据自己的需要选择覆盖,但因为子类如果使用它会报错,对于可修改容器也可以看做是抽象方法吧:

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

比如 ArrayList 对其的实现逻辑是向其维护的动态数组中添加元素,LikedList 对其的实现逻辑是向其维护的链表中添加元素 ... 不同的容器有各种不同的添加元素逻辑。

Java EE 之 HttpServlet

javax.servlet.http.HttpServlet 类在 Java EE(现在是 Jakarta EE)中用于处理 HTTP 请求和响应。

它其实也算是一个模板方法模式的实现,它作为一个抽象类,其中的 service 方法是所有 HTTP 请求处理的入口点。因为对每种请求进行的处理各不相同,只能定义成模板方法,供子类实现:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    // ...
    if (method.equals("GET")) {
        // ...
        this.doGet(req, resp);
    } else if (method.equals("HEAD")) {
        // ...
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
       // ...

}

service 根据请求方法(Method)选择不同的处理方法,如 doGet、doPost,这些方法在HttpServlet 中的默认实现都是返回客户端一个错误码,类似于 AbstractListadd 方法抛出一个异常。

开发人员想要处理什么 Method 的请求,就必须重新对应的方法。

Mybatis 之 BaseExecutor

BaseExecutor 作为一个抽象类,提供了执行数据库操作的基础实现,并定义了一些通用的逻辑,具体的执行策略(如 query 和 update)由其子类实现。

代码节选如下,很容易理解:

public abstract class BaseExecutor implements Executor {
    
    // 执行查询的模板方法
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter) {
        // 通用的查询逻辑
        // ...
        // 调用子类实现的具体方法
        return doQuery(ms, parameter);
    }
    
    // 抽象方法,子类需要实现具体的查询逻辑
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter);
    
    // 其他通用方法,如更新、事务管理等
    @Override
    public int update(MappedStatement ms, Object parameter) {
        // 通用的更新逻辑
        // ...
        return doUpdate(ms, parameter);
    }
    
    protected abstract int doUpdate(MappedStatement ms, Object parameter);
}

具体的 Executor 实现类,如 SimpleExecutorReuseExecutorBatchExecutor 等,则继承了 BaseExecutor,并实现了执行 SQL 操作的具体逻辑。

总结

前边很多设计模式的都有抽象类,这里再强调一下抽象类和接口的区别:

接口是顶层的设计,是对行为的一种抽象,相当于一组协议或者契约,接口实现了约定和实现相分离,降低了代码的耦合性,提高了代码的扩展性。

抽象类是自下而上自动生成的,先有子类的代码重复,然后再抽象成上层的抽象类,以达到代码复用的目的。如果一个抽象类实现了某个接口,那该接口的基本实现逻辑在该抽象类中大概率都能找到。

模版方法模式就是利用抽象类的能力,将子类的重复代码从子类抽离出来,但模板方法模式之所以叫模板方法模式,就是因为抽象类中存在模版方法——一系列步骤构成的“业务逻辑”的算法骨架。

模版方法模式,是根据抽象类实现的,抽象类的好处就是模板方法模式的好处——当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现,抽象类就可以将其提取出来。

如果不变的行为是算法骨架,那该抽象类就可以提取成模版方法

优点如下:

代码复用:通过将不变的算法步骤放在父类中,模板方法模式可以减少代码的重复,促进代码复用。通过将公共的操作步骤放在父类中,也可以减少子类之间的耦合,增加代码的可维护性。

控制算法骨架:父类可以控制整个算法的执行顺序,而子类则可以专注于实现具体的步骤。这种方式提供了更大的灵活性。

增强扩展性:子类可以在不修改父类代码的情况下,扩展或改变算法的部分实现。这样,可以方便地修改或增加新的行为。

缺点如下:

子类依赖父类:子类必须遵循父类中定义的算法骨架,这可能导致子类的实现受到限制,不能自由设计自己的算法。

难以扩展父类:如果父类中的算法需要改变,可能会影响所有继承自该父类的子类,导致潜在的兼容性问题。

复杂性增加:对于复杂的算法,如果父类和子类都涉及很多步骤和变体,可能会增加系统的复杂性和理解难度。

当算法的整体结构固定,但某些具体步骤可以变动时,模板方法模式可以很好地工作。适用于那些步骤的实现变化频繁,但整体流程保持不变的场景。如果算法的步骤高度依赖于具体的实现,使用模板方法模式可能不够灵活或难以实现。