茶的做法和咖啡的做法一样 —— 模板方法模式

181 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。


大家好,我是方圆,第一次在掘金上发文章,就以此来纪念,也要求自己保证每篇文章的质量吧!

1. 茶和咖啡的秘方公布给大家

public class Coffee {

    /**
     * 准备饮品的方法,分以下四步
     */
    public void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugar();
    }

    public void boilWater() {
        System.out.println("把水烧开");
    }

    public void brewCoffeeGrinds() {
        System.out.println("用热水冲咖啡");
    }

    public void pourInCup() {
        System.out.println("倒入杯中");
    }

    public void addSugar() {
        System.out.println("加点儿糖");
    }
}
public class Tea {

    /**
     * 准备饮品的方法,分以下四步
     */
    public void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }

    public void boilWater() {
        System.out.println("把水烧开");
    }

    public void steepTeaBag() {
        System.out.println("用热水冲茶");
    }

    public void pourInCup() {
        System.out.println("倒入杯中");
    }

    public void addLemon() {
        System.out.println("加点儿柠檬");
    }
}
  • 我们从这个秘方中发现,都有把水烧开倒入杯中的步骤,而另外两个方法则是根据饮料不同,而自己特有的,我们可以把重复的代码抽取出来,而特有的归自己保管。通过前两句我的描述,你能想到什么?

想不到

  • 我们看下面这张图,就明白了

模板方法模式.png

1.1 还没理解?很正常,我们用代码实现一下

我们先看基类,其中有一个特别的点,我先不说,看您能不能发现。

public abstract class HotDrink {
    
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    // 以下两个方法,对于茶和咖啡都是相同的
    public void boilWater() {
        System.out.println("把水烧开");
    }
    public void pourInCup() {
        System.out.println("倒入杯中");
    }

    // 以下两个方法子类各不相同
    public abstract void brew();
    public abstract void addCondiments();
}

我们再看看两个实现类,仅是实现了各自所需,没有什么特别

public class Coffee extends HotDrink {
    @Override
    public void brew() {
        System.out.println("用水冲咖啡");
    }

    @Override
    public void addCondiments() {
        System.out.println("加点儿糖");
    }
}
public class Tea extends HotDrink {
    @Override
    public void brew() {
        System.out.println("热水泡茶");
    }

    @Override
    public void addCondiments() {
        System.out.println("加点儿柠檬");
    }
}

1.2 揪一揪其中的小细节

  • 基类中,prepareRecipe()方法,用了final修饰符,这个方法不能被子类修改,做饮料,就按照这个模板来(关键字出现了),这就是模板方法模式,其中具体的步骤,根据具体实现类的不同而有具体的差异。

我们写个测试方法测试一下

public class Test {
    public static void main(String[] args) {
        Tea tea = new Tea();
        Coffee coffee = new Coffee();

        tea.prepareRecipe();
        System.out.println("---------");
        coffee.prepareRecipe();
    }
}

结果如下

在这里插入图片描述

好了,我们大致的了解了,我们把它用官方话说的定义看看,我们能不能从中悟出东西。

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

在这里插入图片描述

通俗了说,模板就是一个方法,这个方法将算法定义成一组步骤,其中任何步骤都可以是抽象的,具体的实现由子类来负责。这样可以保证方法的结构不变,同时子类之间又各有千秋

1.3 这个模板方法模式,有啥优点呐?

  1. 减少了重复的代码,实现了代码的复用,如我们的例子中,泡茶和冲咖啡都有一样的步骤
  2. 引入新的子类容易,子类只需实现自己特有的步骤就好
  3. 模板基类专注于方法本身,子类专注于具体的实现步骤

2. 你听说过吗?代码里会有钩子!

  • 钩子是啥?钩子是一种被声明在抽象类中的方法,可以为空实现或有默认实现

我们在基类里加个钩子看看

public abstract class HotDrink {

    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if(customerWantsCondiments()) {
            addCondiments();
        }
    }

	...
	
    // 这个就是钩子方法...
    public boolean customerWantsCondiments() {
        return true;
    }
}

这个钩子方法起到的是询问顾客是否需要加配料,现在的默认实现都是需要加

2.1 基类,选择性对钩子进行实现

  • 但是我们想知道顾客到底想不想加,所以直接问他!我们在下面添加了一个询问的方法
public class Coffee extends HotDrink {
    @Override
    public void brew() {
        System.out.println("用水冲咖啡");
    }

    @Override
    public void addCondiments() {
        System.out.println("加点儿糖");
    }

    @Override
    public boolean customerWantsCondiments() {
        String ans = getCustomerAnswer();
        if ("y".equals(ans)) {
            return true;
        } else if ("n".equals(ans)) {
            return false;
        } else {
            return false;
        }
    }

    /**
     * 询问顾客是否需要加糖
     */
    private String getCustomerAnswer() {
        String ans = null;

        System.out.println("你需要加糖吗?(y/n)");

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            ans = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (ans == null) {
            return "n";
        }

        return ans;
    }
}

基类重写了钩子方法,实现了询问的功能,简单测试一下

public class Test {
    public static void main(String[] args) {
        Coffee coffee = new Coffee();
        coffee.prepareRecipe();
    }
}

结果如下

在这里插入图片描述

2.2 钩子是个什么样儿的存在?

  • 钩子的存在能够作为条件控制,影响抽象类中的算法流程
  • 钩子在子类中的实现是可选的,它对于子类来说,多数情况下并不重要

2.3 好莱坞原则

  • 基类对实现类之间的原则:基类能对子类进行调用,子类不能调用基类

3. 我们见过的模板方法模式

AbstractList中有一个get的抽象方法,只是没有定义实现,不过也相当于模板方法中的模板,定义了算法的步骤,具体的实现延迟到子类中

图片.png

子类ArrayList对此做了实现,如下

图片.png

:这个模式的侧重点在于,提供一个算法模板,我们在开发中遇到的模板方法模式,不一定非要沿袭继承的方法,像java.io中InputStream类的read()方法,也是需要子类来实现的,但是没有用到继承。


4. 总结

  1. 模板方法模式,定义了算法的步骤,而具体的实现延迟到了子类中
  2. 模板方法模式,让我们实现了代码复用
  3. 模板方法的抽象类可以定义具体方法,抽象方法和钩子
  4. 钩子是一种方法,空实现或默认实现,在子类中可以选择性的重写
  5. 模板的方法声明为final,防止被子类修改

无限进步