Head First 设计模式(八)模板方法模式

定义

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

不结合例子比较难理解,这里还是按我的理解先简单翻译下定义。

模板方法就是一个固定步骤的“算法”骨架方法。这个算法的可变部分通过继承,在子类中重载实现。这样就可以在算法骨架不变的情况下,算法细节步骤根据不同的需求进行适应的改变

场景+代码

场景

假设现在我们要设计泡茶和泡咖啡的一套程序。

我们来看看两者的步骤:

泡茶:

  1. 煮沸水
  2. 加入茶叶冲泡
  3. 根据需求加入调料(如蜂蜜、柠檬)
  4. 将泡好的茶水倒入杯子

咖啡:

  1. 煮沸水
  2. 加入咖啡粉冲泡
  3. 根据需求加入调料(如牛奶、糖)
  4. 将泡好的咖啡倒入杯子

我们发现两者的步骤非常相似,仅有部分细节不一:如泡茶冲的是茶叶,加的是蜂蜜;泡咖啡加的是牛奶

其实泡茶和泡咖啡的过程就是一个固定骨架步骤的“算法”,我们可以抽象为:

  1. 煮沸水
  2. 冲泡
  3. 根据需求加入调料
  4. 将泡好的饮料倒入杯子

标蓝部分为算法中不一样的部分,如何解决?下面,我们用“模板方法模式”来解决这种不一致。

代码

首先,定义一个含有固定骨架“模板方法”的咖啡因饮料抽象类:

/**
 * 咖啡因饮料
 */
public abstract class CaffeineBeverage {
    /**
     * 模板方法,准备饮料
     */
    public final void prepareRecipe(){
        boilWater();
        brew();
        //用于模板方法的算法中可选部分的控制
        if(customerWantsCondiment())
            addCondiment();
        pourInCup();
    }

    /**
     *  煮沸水
     */
    public void boilWater() {
        System.out.println("煮沸水");
    }

    /**
     *  冲泡
     */
    public abstract void brew();

    /**
     *  增加调味剂
     */
    public abstract void addCondiment();

    /**
     * 将饮料倒入杯子
     */
    public void pourInCup() {
        System.out.println("将饮料倒入杯中");
    }

    /**
     * “钩子”方法。顾客决定是否加调料
     */
    public Boolean customerWantsCondiment(){
        return true;
    }
}

这时,准备饮料的四个固定步骤我们都写在模板方法prepareRecipe()里了。这个算法步骤是不可更改的,所以我们给这个模板方法加了final关键字。

然后,根据茶和咖啡在算法步骤上的不同,我们设计两个类,继承抽象方法,分别重载模板方法中的步骤,从而实现茶和咖啡在算法步骤中各自的不同:

public class Tea extends CaffeineBeverage{

    @Override
    public void brew() {
        System.out.println("浸泡茶叶");
    }

    @Override
    public void addCondiment() {
        System.out.println("添加蜂蜜");
    }
}

public class Coffee extends CaffeineBeverage{

    @Override
    public void brew() {
        System.out.println("冲泡咖啡粉");
    }

    @Override
    public void addCondiment() {
        System.out.println("添加糖和牛奶");
    }

}

最后,我们进行测试:

public class Main {
    public static void main(String[] args) {
        CaffeineBeverage tea = new Tea();
        tea.prepareRecipe();
        System.out.println("===============");
        CaffeineBeverage coffee = new Coffee();
        coffee.prepareRecipe();
    }
}/**Output:
煮沸水
浸泡茶叶
添加蜂蜜
将饮料倒入杯中
===============
煮沸水
冲泡咖啡粉
添加糖和牛奶
将饮料倒入杯中
*/

可以看到,通过继承,模板方法在茶和咖啡中的实现有了差别。

模板方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这样可以确保算法的结构保持不变,同时由子类提供部分的实现

所以,模板方法就是定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现

最后,我们给出模板方法的类图:

进一步1:钩子

这样就好像说完了,但我们进一步思考。子类重载步骤,可以确保不同需求步骤的算法的实现。但如果这个算法有些步骤是可省略的呢?

细心的朋友可以发现,先前我们在抽象类中加了“钩子”方法。

什么是“钩子”方法呢?我们来看一下定义:

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

简单点说,钩子就是对算法中步骤的不同(省略或增加)而设计的。在上面的代码中,我们写了一个钩子来决定是否加调料

    /**
     * “钩子”方法。顾客决定是否加调料
     */
    public Boolean customerWantsCondiment(){
        return true;
    }
    /**
     * 模板方法,准备饮料
     */
    public final void prepareRecipe(){
        boilWater();
        brew();
        //用于模板方法的算法中可选部分的控制
        if(customerWantsCondiment())
            addCondiment();
        pourInCup();
    }

现在,我们用一个子类来挂钩:

public class Tea extends CaffeineBeverage{

    @Override
    public void brew() {
        System.out.println("浸泡茶叶");
    }

    @Override
    public void addCondiment() {
        System.out.println("添加蜂蜜");
    }

    //覆盖父类的“钩子”方法,更改算法中的可选部分
    @Override
    public Boolean customerWantsCondiment(){
        //询问顾客是否需要调料
        String answer = askCustomerNeedCondiment();
        if("y".equals(answer))
            return true;
        else
            return false;
    }

    private String askCustomerNeedCondiment() {
        String answer = null;

        System.out.println("请问您要不要加蜂蜜?请回答y或n");
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return answer;
    }
}

这时,我们准备茶水时就能根据顾客的回答而安排需要加调料这一步骤了。子类通过覆盖钩子方法,实现了算法中的可选部分。

进一步2:新的设计原则—好莱坞原则

好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你

简单点说,好莱坞原则就是确保不会出现高层组件依赖底层组件、底层组件又依赖高层组件的“依赖腐败”。只有高层组件会决定什么时候和怎样使用底层组件,而底层组件不会调用高层组件

在模板方法模式中,算法的实现会调用到具体子类的某个方法,也就是高层组件依赖于底层组件。具体子类不会调用父类中的方法,不会形成底层组件依赖高层组件的环状依赖:

用到这个原则的设计模式还有:
工厂方法模式(可看作模板方法模式的一个特殊版本)、装饰者模式……

进一步3:策略模式与模板方法模式比较

策略模式和模板方法模式很像,都是针对算法改变的情况的设计模式。

我们对比下两者的定义:

策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

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

很明显可以看出:

  • 策略模式是采用的组合来实现算法的变化,这样的设计更加灵活,依赖性程度低;

  • 模板方法模式采用的继承来实现算法中的变化部分,这样的设计对算法有更多的控制权,且代码的重复会少一些,但由于算法依赖于父类,所以依赖程度高。

Java中的模板方法模式

Java中较常见的模板方法模式的应用就是数组的排序了,例如:

Arrays.sort(Object[]);

Object对象实现了Comparable接口,排序通过Comparable接口中的compareTo()实现。

注意,这个排序的例子表面看上去好像与模板方法模式无关(没有用到继承),但实质仍是通过子类提供算法步骤的实现来实现了算法的变化。虽然采用了组合,但思想仍是模板方法模式的思想。


本文总结自:

《Head First 设计模式》第八章:模板方法模式

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值