模板方法模式的原理和代码实现都比较简单,在软件开发中也被广泛应用,但是因为使用继承机制,副作用往往盖过了主要作用,所以在使用时尤其要小心谨慎。

一、模式原理分析

模板方法模式原始定义是:在操作中定义算法的框架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。

从这个定义中,我们能看出模板方法模式的定位很清楚,就是为了解决算法框架这类特定的问题,同时明确表示需要使用继承的结构

模板方法模式包含关键的两个角色:

  • 抽象父类:定义一个算法所包含的所有步骤,并提供一些通用的方法逻辑。

  • 具体子类:继承自抽象父类,根据需要重写父类提供的算法步骤中的某些步骤。

模板方法模式的代码实现:

public abstract class AbstractClassTemplate {
    void step1(String key){
        //dosomthing
        System.out.println("=== 在模板类里 执行步骤 1");
        if (step2(key)) {
            step3();
        } else {
            step4();
        }
        step5();
    }
    boolean step2(String key){
        System.out.println("=== 在模板类里 执行步骤 2");
        if ("x".equals(key)) {
            return true;
        }
        return false;
    }
    abstract void step3();
    abstract void step4();
    void step5() {
        System.out.println("=== 在模板类里 执行步骤 5");
    }
    void run(String key){
        step1(key);
    }
}
public class ConcreteClassA extends AbstractClassTemplate {
    @Override
    void step3() {
        System.out.println("===在子类 A 中 执行:步骤3");
    }
    @Override
    void step4() {
        System.out.println("===在子类 A 中 执行:步骤4");
    }
}
public class ConcreteClassB extends AbstractClassTemplate {
    @Override
    void step3() {
        System.out.println("===在子类 B 中 执行:步骤3");
    }
    @Override
    void step4() {
        System.out.println("===在子类 B 中 执行:步骤4");
    }
}
public class Demo {
    public static void main(String[] args) {
        AbstractClassTemplate concreteClassA = new ConcreteClassA();
        concreteClassA.run("");
        System.out.println("===========");
        AbstractClassTemplate concreteClassB = new ConcreteClassB();
        concreteClassB.run("x");
    }
}
//输出结果:
=== 在模板类里 执行步骤 1
=== 在模板类里 执行步骤 2
===在子类 A 中 执行:步骤4
=== 在模板类里 执行步骤 5
===========
=== 在模板类里 执行步骤 1
=== 在模板类里 执行步骤 2
===在子类 B 中 执行:步骤3
=== 在模板类里 执行步骤 5

二、为什么使用模板方法模式?

使用模板方法模式的原因主要有两个。

第一个,期望在一个通用的算法或流程框架下进行自定义开发。 比如,在使用 Jenkins 的持续集成发布系统中,可以定制一个固定的 Jenkins Job 任务,将打包、发布、部署的流程作为一个通用的流程。对于不同的系统来说,只需要根据自身的需求增加步骤或删除步骤,就能轻松定制自己的持续发布流程。

第二个,避免同样的代码逻辑进行重复编码。比如,当调用 HTTP 接口时,我们会使用 HttpClient、OkHttp 等工具类进行二次开发,不过我们经常会遇见这样的情况,明明有人已经封装过同样的功能,但还是忍不住想要对这些工具类进行二次封装开发,实际上最终实现的功能都只是调用 HTTP 接口,这样“重复造轮子”会非常浪费开发时间。这时如果使用模板方法模式定义个统一的调用 HTTP 接口的逻辑,就能很好地避免重复编码。

三、使用场景分析

模板方法模式的使用场景一般有:

  • 多个类有相同的方法并且逻辑可以共用时;

  • 将通用的算法或固定流程设计为模板,在每一个具体的子类中再继续优化算法步骤或流程步骤时;

  • 重构超长代码时,发现某一个经常使用的公有方法。

为了更好地理解模板方法模式的使用场景,这里我们通过一个具体的示例来讲解。假设我们正在设计一个简单的持续集成发布系统——研发部开发的代码放在 GitLab 上,并使用一个固定的发布流程来进行程序的上线发布,我们打算使用模板方法模式来定义一系列规范的流程。

首先,我们定义一个通用的流程框架类 DeployFlow,定义的步骤有六步,分别是:从 GitLab 上拉取代码、编译打包、部署测试环境、测试、上传包到线上环境、启动程序。其中,从 GitLab 上拉取代码、编译打包、部署测试环境和测试这四个步骤,需要子类来进行实现,代码如下:

public abstract class DeployFlow {
    //使用final关键字来约束步骤不能轻易修改
    public final void buildFlow() {
        pullCodeFromGitlab(); //从GitLab上拉取代码
        compileAndPackage();  //编译打包
        copyToTestServer();   //部署测试环境
        testing();            //测试
        copyToRemoteServer(); //上传包到线上环境
        startApp();           //启动程序
    }
    public abstract void pullCodeFromGitlab();
    public abstract void compileAndPackage();
    public abstract void copyToTestServer();
    public abstract void testing();
    private void copyToRemoteServer() {
        System.out.println("统一自动上传 启动App包到对应线上服务器");
    }
    private void startApp() {
        System.out.println("统一自动 启动线上App");
    }
}

我们分别实现两个子类:①实现本地的打包编译和上传;②实现全自动化的持续集成式的发布。

public class LocalDeployFlow extends DeployFlow{
    @Override
    public void pullCodeFromGitlab() {
        System.out.println("手动将代码拉取到本地电脑......");
    }
    @Override
    public void compileAndPackage() {
        System.out.println("在本地电脑上手动执行编译打包......");
    }
    @Override
    public void copyToTestServer() {
        System.out.println("手动通过 SSH 上传包到本地的测试服务......");
    }
    @Override
    public void testing() {
        System.out.println("执行手工测试......");
    }
}
public class CicdDeployFlow extends DeployFlow{
    @Override
    public void pullCodeFromGitlab() {
        System.out.println("持续集成服务器将代码拉取到节点服务器上......");
    }
    @Override
    public void compileAndPackage() {
        System.out.println("自动进行编译&打包......");
    }
    @Override
    public void copyToTestServer() {
        System.out.println("自动将包拷贝到测试环境服务器......");
    }
    @Override
    public void testing() {
        System.out.println("执行自动化测试......");
    }
}

我们再来运行一个单元测试,测试一下本地发布 LocalDeployFlow 和持续集成发布 CicdDeployFlow。

public class Client {
    public static void main(String[] args) {
        System.out.println("开始本地手动发布流程======");
        DeployFlow localDeployFlow = new LocalDeployFlow();
        localDeployFlow.buildFlow();
        System.out.println("********************");
        System.out.println("开始 CICD 发布流程======");

        DeployFlow cicdDeployFlow = new CicdDeployFlow();
        cicdDeployFlow.buildFlow();
    }
}
//输出结果
开始本地 手动发布流程======
手动将代码拉取到本地电脑......
在本地电脑上手动执行 编译打包......
手动通过 SSH 上传包 到 本地的测试服务......
执行手工测试......
统一自动上传 启动App包到对应线上服务器
统一自动 启动线上App
********************
开始 CICD 发布流程======
持续集成服务器将代码拉取到节点服务器上......
自动进行编译&打包......
自动将包拷贝到测试环境服务器......
执行 自动化 测试......
统一自动上传 启动App包到对应线上服务器
统一自动 启动线上App

虽然例子比较简单,但是充分展示了模板方法模式的使用方法,也就是需要设置一个父类用于定义整个的算法或流程框架,然后让各自的子类去优化不同的算法步骤细节。

从上面场景的分析中,我们能看出,模板方法模式应用场景的最大特征在于,通常是对算法的特定步骤进行优化,而不是对整个算法进行修改。一旦整体的算法框架被定义完成,子类便无法进行直接修改,因为子类的使用场景直接受到了父类场景的影响

四、模板方法模式的优缺点是什么?

通过上面的分析,我们可以得出使用模板方法模式主要有以下两个优点。

  • 有效去除重复代码。 模板方法模式的父类保存通用的代码逻辑,这样可以让子类不再需要重复处理公用逻辑,只用关注特定的逻辑,从而起到去除子类中重复代码的目的。

  • 有助于找到更通用的模板。 由于子类间重复的代码逻辑都会被抽取到父类中,父类也就慢慢变成了更通用的模板,这样有助于积累更多通用的模板,提升代码复用性和扩展性。

当然,模板方法模式也有一些缺点。

  • 不符合开闭原则。 一个父类调用子类实现操作,通过子类扩展增加新的行为,但是子类执行的结果便会受到父类的影响,不符合开闭原则的“对修改关闭”。

  • 增加代码阅读的难度。 由于父类的某些步骤或方法被延迟到子类执行,那么需要跳转不同的子类阅读代码逻辑,如果子类的数量很多的话,跳转会很多,不方便联系上下文逻辑线索。而且模板方法中的步骤越多,其维护工作就可能会越困难。

  • 违反里氏替换原则。 虽然模板方法模式中的父类会提供通用的实现方法,但是延迟到子类的操作便会变成某种定制化的操作,一旦替换子类,可能会导致父类不可用或整体逻辑发生变化。

模板方法模式是一个比较常用的结构型设计模式,它能帮助我们快速构建一个通用模板,对于固定流程、通用协议而言,模板方法模式都是一个很好的选择。

虽然模板方法模式简单实用有很多优势,但是劣势同样明显。比如,继承的结构容易带来“修改一个类而影响所有类”的情况,另外,阅读代码的体验也不是很好,经常需要各个类之间不断切换,并且要想搞清楚整个算法完整的逻辑,可能需要阅读所有子类和父类的代码逻辑。

总之,模板方法模式的使用范围通常比较局限,当我们的子类和父类在频繁进行修改的时候,就需要重新评估算法步骤是否合理。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发

个人小工具程序上线啦,通过公众号(服务端技术精选)菜单【个人工具】即可体验,欢迎大家体验后提出优化意见