• 作者:老汪软件技巧
  • 发表时间:2024-09-03 10:02
  • 浏览量:

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

一、模式原理分析

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

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

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

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

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

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

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

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

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

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

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

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

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