模板方法模式

模板方法模式

冲泡咖啡

有些人没有咖啡就活不下去,有的人则离不开茶,两者的共同成分是什么?当然是咖啡因了。不仅仅如此,他们的冲泡方法都一样。咖啡的冲泡步骤是:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 倒进杯子
  4. 加糖和牛奶

而冲茶的步骤是:

  1. 把水煮沸

  2. 用沸水冲泡茶叶

  3. 倒进杯子

  4. 加柠檬

V1版

让我们快速搞定V1版本的咖啡和茶制作。
咖啡类

/**
 * 咖啡
 */
public class CoffeeV1 {

    public void  prepareRecipe(){
        boilWater(); //把水煮沸
        brewCoffeeGrinds(); //用沸水冲泡咖啡
        pourInCup();  //把咖啡到入杯子
        addSugarAndMilk();  //加糖和加奶
    }

    private void boilWater() {
        System.out.println("把水煮沸");
    }

    private void brewCoffeeGrinds() {
        System.out.println("用沸水冲泡咖啡");
    }

    private void pourInCup() {
        System.out.println("到入杯子");
    }

    private void addSugarAndMilk() {
        System.out.println("加糖和加奶");
    }
}

茶类

/**
 * 茶
 */
public class TeaV1 {

    public void  prepareRecipe(){
        boilWater(); //把水煮沸
        steepTeaBag(); //用沸水冲泡茶
        pourInCup();  //把茶到入杯子
        addLemon();  //加柠檬
    }

    private void boilWater() {
        System.out.println("把水煮沸");
    }

    private void steepTeaBag() {
        System.out.println("用沸水冲泡茶");
    }

    private void pourInCup() {
        System.out.println("到入杯子");
    }

    private void addLemon() {
        System.out.println("加柠檬");
    }

}

测试

/**
 * 测试
 */
public class BeverageMainV1 {
    public static void main(String[] args) {
        CoffeeV1 coffee = new CoffeeV1();
        coffee.prepareRecipe();

        System.out.println();
        TeaV1 tea = new TeaV1();
        tea.prepareRecipe();
    }
}

在咖啡类和茶类中,明显有代码重复,当你发觉有重复的代码出现时,就是给你一个信号,要重构你的代码了。设计模式通常都是在重构代码的时候再引入的。

咖啡类和茶类的重复代码需要抽取出来放到一个地方,这儿我们从咖啡类和茶类抽象出饮料基类,将公共的代码放到饮料基类中。
你可以想象下,如果不能抽象出基类时,又该怎么消除重复的代码?

V2版

基于这个抽象思路,我们来实现V2版
公共的饮料基类,为了不让直接实例化,定义为abstract的。

/**
 * 饮料基类
 */
public abstract class BaseBeverageV2 {

    protected void boilWater() {
        System.out.println("把水煮沸");
    }
    protected void pourInCup() {
        System.out.println("到入杯子");
    }

}

V2版咖啡类,继承饮料基类

/**
 * 咖啡
 */
public class CoffeeV2 extends BaseBeverageV2{

    public void  prepareRecipe(){
        boilWater(); //把水煮沸
        brewCoffeeGrinds(); //用沸水冲泡咖啡
        pourInCup();  //把咖啡到入杯子
        addSugarAndMilk();  //加糖和加奶
    }

    private void brewCoffeeGrinds() {
        System.out.println("用沸水冲泡咖啡");
    }

    private void addSugarAndMilk() {
        System.out.println("加糖和加奶");
    }
}

同样,V2版茶类继承饮料基类

/**
 * 茶
 */
public class TeaV2 extends BaseBeverageV2{

    public void  prepareRecipe(){
        boilWater(); //把水煮沸
        steepTeaBag(); //用沸水冲泡茶
        pourInCup();  //把茶到入杯子
        addLemon();  //加柠檬
    }

    private void steepTeaBag() {
        System.out.println("用沸水冲泡茶");
    }

    private void addLemon() {
        System.out.println("加柠檬");
    }

}

测试

public class BeverageMainV2 {
    public static void main(String[] args) {
        CoffeeV2 coffee = new CoffeeV2();
        coffee.prepareRecipe();

        System.out.println();
        TeaV2 tea = new TeaV2();
        tea.prepareRecipe();
    }
}

嗯,再看一眼,发现咖啡和饮料还有共同点,他们都有prepareRecipe方法,并且步骤差不多,能不能将prepareRecipe方法也放到基类中,我们合并下V2版本中两个子类的prepareRecipe依赖的方法,一共有6个,其中brewCoffeeGrinds()和steepTeaBag()两个方法都是冲泡含义,重命名为brew(),同样addLemon()和addSugarAndMilk()类似,都是加调料,重名为一个addCondiments().
这样prepareRecipe方法中会依赖4个方法,有两个方法是基类已经实现了的,brew()和ddCondiments()因为只有子类知道如何处理,定义为抽象方法。

V3版

看看V3版实现,饮料基类中定义了prepareRecipe方法,并且定义了它依赖的4个步骤,有的步骤基类中就可以实现,而有的步骤需要子类实现。

/**
 * 饮料基类
 */
public abstract class BaseBeverageV3 {
    /**
     * 模板方法
     */
    public void  prepareRecipe(){
        boilWater(); //把水煮沸
        brew(); //用沸水冲泡
        pourInCup();  //到入杯子
        addCondiments();  //加调料
    }

    protected abstract void brew();

    protected abstract void addCondiments();

    protected void boilWater() {
        System.out.println("把水煮沸");
    }
    protected void pourInCup() {
        System.out.println("到入杯子");
    }

}

咖啡子类,实现基类中的抽象方法

/**
 * 咖啡
 */
public class CoffeeV3 extends BaseBeverageV3{

    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加糖和加奶");
    }
}

同理,茶子类,也实现基类中的抽象方法

/**
 * 茶
 */
public class TeaV3 extends BaseBeverageV3{

    protected void brew() {
        System.out.println("用沸水冲泡茶");
    }

    protected void addCondiments() {
        System.out.println("加柠檬");
    }
}

测试

public class BeverageMainV3 {
    public static void main(String[] args) {
        CoffeeV3 coffee = new CoffeeV3();
        coffee.prepareRecipe();

        System.out.println();
        TeaV3 tea = new TeaV3();
        tea.prepareRecipe();
    }
}

在V3实现中,我们就基本上使用模板方法模式来重构了我们的代码,消除代码重复。

V3代码的UML图
在这里插入图片描述

定义

好了,真是认识下模板方法设计模式,模板方法定义了(实现某个功能所需要的)算法的步骤,允许子类提供其中某些步骤的实现。
通常为了不让子类有机会覆盖基类的算法骨架,模板方法定义为final的。

用代码实现下定义吧
抽象类

/**
 * 包含了模板方法的抽象类
 */
public abstract class AbstractClass {

    /**
     * final的模板方法
     */
    public final void templateMethod(){
        primitiveOperation1(); //原语操作1
        primitiveOperation2(); //原语操作2
        concreteOperation();  //具体操作
    }

    protected abstract void primitiveOperation1();

    protected abstract void primitiveOperation2();

    private void concreteOperation() {
        System.out.println("抽象类中,具体的操作。。。");
    }

}

具体类

/**
 * 具体类 - 实现算法某些步骤
 */
public class ConcreteClass extends AbstractClass {
    @Override
    protected void primitiveOperation1() {
        System.out.println("实现原语操作1....");
    }

    @Override
    protected void primitiveOperation2() {
        System.out.println("实现原语操作2....");
    }
}

测试

public class TemplateMethodMain {
    public static void main(String[] args) {
        AbstractClass abstractClass = new ConcreteClass();
        abstractClass.templateMethod();
    }
}

看看UML图
在这里插入图片描述

在上面模板方法定义中,模板方法中包含两种方法,一种是抽象的原语方法,其需要子类提供实现的;还要一类是基类中提供实现的方法,可直接使用,当然,如果不希望子类覆盖,也可以将这类方法定义为final的。第二类方法,有一些特例,比如空实现、默认实现的方法,子类可以选择覆盖或者不覆盖,区别于抽象方法,子类必须实现,这种用作特殊用途的方法,我们称之为钩子方法。

钩子方法

比如,在我们的饮料示例中,我们可以在模板方法中应用钩子,由子类来决定是否要加调料,来看看效果。

/**
 * 饮料基类
 */
public abstract class BaseBeverageV4 {
    /**
     * 模板方法
     */
    public final void  prepareRecipe(){
        boilWater(); //把水煮沸
        brew(); //用沸水冲泡
        pourInCup();  //到入杯子
        if(wantsCondiments()) {
            addCondiments();  //加调料
        }
    }

    /**
     * 子类可以选择是否覆盖该方法,控制模板方法中是否要执行加调料步骤
     * @return
     */
    protected boolean wantsCondiments() {
        return true;
    }

    protected abstract void brew();

    protected abstract void addCondiments();

    protected final void boilWater() {
        System.out.println("把水煮沸");
    }
    protected final void pourInCup() {
        System.out.println("到入杯子");
    }

}

wantsCondiments方法,就是我们说的钩子方法。当然你没必要记住叫什么方法,可以感受和理解这种使用方式即可。
接下来,咖啡类,不覆盖钩子方法,即使用默认实现

/**
 * 咖啡
 */
public class CoffeeV4 extends BaseBeverageV4{

    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加糖和加奶");
    }
}

而茶覆盖了钩子方法,不加入调料

public class TeaV4 extends BaseBeverageV4{
    @Override
    protected boolean wantsCondiments() {
        return false;
    }

    protected void brew() {
        System.out.println("用沸水冲泡茶");
    }

    protected void addCondiments() {
        System.out.println("加柠檬");
    }
}

测试

public class BeverageMainV4 {
    public static void main(String[] args) {
        CoffeeV4 coffee = new CoffeeV4();
        coffee.prepareRecipe();

        System.out.println();
        TeaV4 tea = new TeaV4();
        tea.prepareRecipe();
    }
}

输出

把水煮沸
用沸水冲泡咖啡
到入杯子
加糖和加奶

把水煮沸
用沸水冲泡茶
到入杯子

可以看出,模板方法模式在实际使用中,是非常灵活的,怎么顺手怎么来。

扩展示例

在看一个数据处理示例,示例参考: https://java2blog.com/template-method-design-pattern-in-java/
通常数据处理的整体步骤是,读取数据,处理数据,写数据。这个示例中,演示从两种数据源,数据库和csv文件读取数据,然后处理数据,最后写CSV文件。
抽象的数据处理器,里面定义了数据处理的模板方法:

/**
 * 抽象的数据处理器,定义数据处理的模板方法
 */
public abstract class DataParser {
    //Template method
    //This method defines a generic structure for parsing data
    public void parseDataAndGenerateOutput() {
        readData();
        processData();
        writeData();
    }
    //This methods will be implemented by its subclass
    abstract void readData();
    abstract void processData();

    //We have to write output in a CSV file so this step will be same for all subclasses
    public void writeData() {
        System.out.println("Output generated,writing to CSV");
    }
}

CSVDataParser实现


public class CSVDataParser extends DataParser {
    @Override
    void readData() {
        System.out.println("Reading data from csv file");
    }

    @Override
    void processData() {
        System.out.println("Looping through loaded csv file");
    }
}

DatabaseDataParser实现

public class DatabaseDataParser extends DataParser {
    @Override
    void readData() {
        System.out.println("Reading data from database");
    }

    @Override
    void processData() {
        System.out.println("Looping through datasets");
    }
}

测试

public class DataParserMain {
    public static void main(String[] args) {
        CSVDataParser csvDataParser=new CSVDataParser();
        csvDataParser.parseDataAndGenerateOutput();
        System.out.println("**********************");
        DatabaseDataParser databaseDataParser=new DatabaseDataParser();
        databaseDataParser.parseDataAndGenerateOutput();
    }
}

数据处理的UML图
在这里插入图片描述

模板方法在通常框架中使用较多,比如HttpServlet。
说到这里,从代码调用的角度,框架和库的区别是?框架调用你的代码,你的代码调用库API。

源码

https://gitee.com/cq-laozhou/design-pattern

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值