公号:码农充电站pro

主页:https://codeshellme.github.io

今天来介绍模板方法模式Template Method Design Pattern)。

1,制作饮料的过程

在这里插入图片描述

假如我们要制作两种饮料:苹果饮料和橙子饮料。这两种饮料的制作流程如下:

  • 苹果饮料制作流程
    • 把苹果榨成苹果汁
    • 将苹果汁倒入杯中
    • 向杯中倒入水
    • 根据客户喜好是否放入白糖
      • 喜欢则放入白糖,否则不放白糖
  • 橙子饮料制作流程
    • 把橙子榨成橙汁
    • 将橙汁倒入杯中
    • 向杯中倒入水
    • 根据客户喜好是否放入白糖
      • 喜欢则放入白糖,否则不放白糖

2,模拟制作饮料

如果要模拟饮料的制作过程,按照最直接的想法,我们创建两个类 AppleBeverageOrangeBeverage 分别用于制作苹果饮料和橙子饮料。

首先根据苹果饮料的制作流程编写 AppleBeverage 类:

class AppleBeverage {
    private boolean isSweet;

    public AppleBeverage(boolean isSweet) {
        this.isSweet = isSweet;
    }

    // 把苹果榨成苹果汁
    public void squeezeAppleJuice() {
        System.out.println("squeeze apple juice");
    }

    // 将苹果汁倒入杯中
    public void appleJuiceToCup() {
        System.out.println("pour the apple juice into the cup");
    }

    // 向杯中倒入水
    public void waterToCup() {
        System.out.println("pour water into the cup");
    }

    // 向杯中倒入白糖
    public void sugarToCup() {
        System.out.println("pour sugar into the cup");
    }

    // 制作苹果饮料
    public void makeAppleBeverage() {
        squeezeAppleJuice();
        appleJuiceToCup();
        waterToCup();

        if (isSweet) {
            sugarToCup();
        }
    }
}

再根据橙子饮料的制作流程编写 OrangeBeverage 类:

class OrangeBeverage {
    private boolean isSweet;

    public OrangeBeverage(boolean isSweet) {
        this.isSweet = isSweet;
    }

    // 把橙子榨成橙汁
    public void squeezeOrangeJuice() {
        System.out.println("squeeze orange juice");
    }

    // 将橙汁倒入杯中
    public void orangeJuiceToCup() {
        System.out.println("pour the orange juice into the cup");
    }

    // 向杯中倒入水
    public void waterToCup() {
        System.out.println("pour water into the cup");
    }

    // 向杯中倒入白糖
    public void sugarToCup() {
        System.out.println("pour sugar into the cup");
    }

    // 制作橙子饮料
    public void makeOrangeBeverage() {
        squeezeOrangeJuice();
        orangeJuiceToCup();
        waterToCup();

        if (isSweet) {
            sugarToCup();
        }
    }
}

3,分析代码

可以看到上面两个类的代码非常简单,为了更加详细的分析,我画出了这两个类的类图:

在这里插入图片描述

我将这两个类中的方法相同的部分用蓝色标了出来,可以看到,这两个类中的 waterToCupsugarToCup 方法一模一样,其它三个方法也是非常的相似。

这样的代码显然是没有复用已有的代码。

4,改进代码

那么,自然而然,我们可以将两个类中相同的部分,抽象出来放入一个父类中,然后不同的部分让子类去实现。

因此,我们可以编写出父类,如下:

abstract class Beverage {
    protected boolean isSweet;

    // 榨果汁
    public abstract void squeezeJuice();
    
    // 将果汁倒入杯中
    public abstract void juiceToCup();
    
    // 向杯中倒入水
    public void waterToCup() {
        System.out.println("pour water into the cup");
    }

    // 向杯中倒入白糖
    public void sugarToCup() {
        System.out.println("pour sugar into the cup");
    }

    // 制作苹果饮料
    public final void makeBeverage() {
        squeezeJuice();
        juiceToCup();
        waterToCup();
        
        // 根据喜好是否加白糖
        if (isSweet) {
            sugarToCup();
        }
    }
}

我们将所有相同的代码都抽取到了 Beverage 类中,相同的部分有:

  • isSweet 变量
  • waterToCup 方法
  • sugarToCup 方法
  • makeBeverage 方法

其中 makeBeverage 方法使用了 final 关键字来修饰,表示我们不希望子类去修改它。

不同的部分有:

  • squeezeJuice 方法
  • juiceToCup 方法

这两个方法都是抽象方法,表示我们希望子类根据自己的需求去实现。最终在子类中调用 makeBeverage 方法时,makeBeverage 会依据多态性来调用正确的 squeezeJuicejuiceToCup 方法。

下面编写 AppleBeverage 类:

class AppleBeverage extends Beverage {
    public AppleBeverage(boolean isSweet) {
        this.isSweet = isSweet;
    }

    public void squeezeJuice() {
        System.out.println("squeeze apple juice");
    }

    public void juiceToCup() {
        System.out.println("pour the apple juice into the cup");
    }
}

AppleBeverage 继承了 Beverage,并且实现了 squeezeJuicejuiceToCup 方法。

再编写 OrangeBeverage 类:

class OrangeBeverage extends Beverage {
    public OrangeBeverage(boolean isSweet) {
        this.isSweet = isSweet;
    }

    public void squeezeJuice() {
        System.out.println("squeeze orange juice");
    }

    public void juiceToCup() {
        System.out.println("pour the orange juice into the cup");
    }
}

OrangeBeverage 继承了 Beverage,并且实现了 squeezeJuicejuiceToCup 方法。

经过改进后的代码类图如下:

在这里插入图片描述

可以看到经过改进的代码,重复的代码都抽取到了父类中,能复用的代码都进行了复用,子类只需根据自己的需要实现父类的抽象方法就行。

我将所有代码放在了这里,供大家参考。

5,模板方法

实际上,上面代码的实现方式就使用到了模板方法模式

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

这里的算法指的是实际项目中的业务逻辑

模板方法的类图很简单,如下:

在这里插入图片描述

模板方法模式的重点在于,它在父类中定义了一个通用的算法框架templateMethod),也就是上面代码中的 makeBeverage 方法,这个方法就是模板方法,一般用 final 修饰,用于防止子类覆盖。

另外还有一些抽象方法,这些抽象方法都用 abstract 进行了修饰,表示必须由子类实现。算法框架调用了这些抽象方法,这样就相当于子类重新定义了算法中的某些步骤。

在上面代码的 makeBeverage 方法中还用到了一个变量 isSweet,这个变量在子类对象中的不同取值,会影响到 makeBeverage 的执行流程。

这个 isSweet 变量叫作“钩子”,钩子可以是一个变量,也可以是一个方法,它可以改变模板方法的执行流程。

6,总结

模板方法模式提供了一个算法步骤,从中我们能看到代码复用的技巧。

模板方法模式中的抽象方法由子类实现,这意味着父类定义了算法的框架流程,而将算法的实现延迟到了子类中。

我们通常会将模板方法工厂方法放在一起比较,这两个模式有一个明显的不同点,就是模板方法模式将一个算法流程中的某些步骤的具体实现延迟到了子类中,而工厂方法模式是将对象的创建延迟到了子类中。

Java JDK 中,我们能看到很多模板方法的应用案例,比如 InputStream.read(byte b[], int off, int len) 方法。

(本节完。)


推荐阅读:

观察者模式-将消息通知给观察者

装饰者模式-动态的包装原有对象的行为

命令模式-将请求封装成对象

适配器模式-让不兼容的接口得以适配

外观模式-简化子系统的复杂性


欢迎关注作者公众号,获取更多技术干货。

码农充电站pro