[设计模式笔记] No.8 模板方法模式(Template)

模板方法模式,它以及它的变体,在我们编程的时候经常见到。

定义模板方法模式

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

这个模式使用了创建一个算法的模板,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责是实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。

什么个意思呢,通过下面案例解释下:

冲咖啡:

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

泡茶:

1、把水煮沸
2、用沸水冲泡茶叶
3、把茶倒进杯子
4、加柠檬

相信大部分程序员都会这样去设计类:

咖啡——Coffee
public class Coffee {
    /**
     * 冲泡咖啡算法
     */
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugerAndMilk();
    }

    /**
     *
     */
    private void boilWater() {
        System.out.println("Boiling water");
    }

    /**
     * 冲泡咖啡
     */
    private void brewCoffeeGrinds() {
        System.out.println("Dripping coffee through filter");
    }

    /**
     * 把咖啡倒进杯子
     */
    private void pourInCup() {
        System.out.println("Pouring into cup");
    }

    /**
     * 加糖和奶
     */
    private void addSugerAndMilk() {
        System.out.println("Adding Sugar and Milk");
    }
}
茶——Tea
public class Tea {

    /**
     * 泡茶算法
     */
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }

    /**
     * 煮沸水。这个方法和咖啡类完全一样
     */
    private void boilWater() {
        System.out.println("Boiling water");
    }

    /**
     * 冲泡茶叶
     */
    private void steepTeaBag() {
        System.out.println("Steeping the tea");
    }

    /**
     * 把茶倒进杯子。这个方法和咖啡类完全一样
     */
    private void pourInCup() {
        System.out.println("Pouring into cup");
    }

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

从上面两个类,我们可以看出,冲泡咖啡和茶,有相同的步骤,比如 煮沸水boilWater(),倒进杯子pourInCup(),有些步骤很相似 冲泡咖啡和冲泡茶叶,加糖和奶跟加柠檬。
重构: 抽取基类CaffeineBeverage(咖啡因) 用于控制 咖啡和茶 冲泡的算法;
抽取相同的代码:boilWater()还有pourInCup();
抽象冲泡咖啡和冲泡茶叶方法,让子类实现具体的步骤, 统一用 brew()(冲泡)替换;
抽象加糖和奶跟加柠檬方法,让子类实现具体的步骤, 统一用 addCondiments()(添加调料)替换;

CaffeineBeverage代码如下
public abstract class CaffeineBeverage {
    /**
     * 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
     * prepareRecipe()方法被声明为final,因为我们不希望子类覆盖这个方法
     * 我们将第2步和第4步泛化成为brew()和addCondiments()
     */
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    /**
     * 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,
     * 剩余的东西留给子类去操心
     */
    abstract void addCondiments();

    abstract void brew();

    public void boilWater() {
        System.out.println("Boiling water");
    }

    public void pourInCup() {
        System.out.println("Pouring into cup");
    }
}

Coffee

public class Coffee extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("Dripping coffee through filter");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }
}

Tea

public class Tea extends CaffeineBeverage {
    @Override
    void brew() {
        System.out.println("Steeping the tea");
    }

    @Override
    void addCondiments() {
        System.out.println("Adding Lemon");
    }
}

上面就是模板方法的应用

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

从 CaffeineBeverage 类中,了解 模板方法特点

  • 由CaffeineBeverage 类主导一切,它拥有算法,而且保护这个算法。
  • 对于子类来说,CaffeineBeverage 类的存在,可以将代码的复用最大化。
  • 算法只存在于一个地方,所以很容易修改。
  • 这个模板方法提供了一个框架,可以让其他的咖啡因饮料插进来。新的咖啡因饮料只需要实现自己的方法就可以了。
  • CaffeineBeverage 类专注在算法本身,而由子类提供完整的实现。
模板方法模式扩展——钩子

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

下面我们给 CaffeineBeverage 类加上钩子,让子类控制是否加入调料。

public abstract class CaffeineBeverage {
    /**
     * 现在,用同一个prepareRecipe()方法来处理茶和咖啡。
     * prepareRecipe()方法被声明为final,因为我们不希望子类覆盖这个方法
     * 我们将第2步和第4步泛化成为brew()和addCondiments()
     */
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        /**
         *  我们加上了一个小小的条件语句,而该条件是否成立,
         *  是由一个具体方法customerWantsCondiments()决定的。
         *  如果顾客“想要”调料,只有这时我们才调用addCondiments()。
         */
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    /**
     * 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象,
     * 剩余的东西留给子类去操心
     */
    abstract void addCondiments();

    abstract void brew();

    public void boilWater() {
        System.out.println("Boiling water");
    }

    public void pourInCup() {
        System.out.println("Pouring into cup");
    }

    /**
     * 我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
     * 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做。
     *
     * @return
     */
    protected boolean customerWantsCondiments() {
        return true;
    }
}

如上面代码中,customerWantsCondiments() 方法就是传说中的钩子,用于让子类自行控制是否“想要”调料。

设计原则八——好莱坞原则

别调用我们,我们会调用你。

好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件有依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。


[设计模式笔记] No.8 模板方法模式(Template)_第1张图片
template_01.jpg
要点
  • “模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类。
  • 模板方法模式为我们提供了一种代码复用的重要技巧。
  • 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
  • 抽象方法由子类实现。
  • 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。
  • 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
  • 好莱坞原则告诉我们,将决策权放在高层模板中,以便决定如何以及何时调用低层模板。

感谢你的耐心阅读,模板方法模式基本知识和应用就介绍到这里了。

最后回顾一下

我们的设计工具箱中的工具
1、OO基础

① 抽象
② 封装
③ 多态
④ 继承

2、OO原则

① 封装变化
② 多用组合,少用继承
③ 针对接口编程,不针对实现编程
④ 为交互对象之间的松耦合设计而努力
⑤ 类应该对扩展开放,对修改关闭
⑥ 依赖抽象,不要依赖具体编程
⑦“最少知识”原则:只和朋友交谈
⑧“好莱坞”原则:别调用我,我会调用你。

3、OO模式

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

② 观察者模式——在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会受到通知并自动更新。

③ 装饰者模式——动态地将责任附加到对象上,若要扩展功能,装饰者提供有别于继承的另一种选择。

④ 工厂方法模式——定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
抽象工厂模式——提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

⑤单件模式——确保一个类只有一个实例,并提供全局访问点。

⑥命令模式——将“请求”封装成对象,这可以让你使用不同的请求、队列或者日志来参数化其他对象。命令模式也可以支持撤销操作。

⑦适配器模式——将一个类的接口,转换成客户所期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
外观模式——提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

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

感谢阅读!
No.7 适配器模式(Adapter)和外观模式(Facade)
No.6 命令模式(Command)
No.5 单件模式(singleton )
No.4 工厂模式(Factory)
No.3 装饰者模式(Decorator)
No.2 观察者模式(Observer)
No.1 策略模式(Strategy)
前言 为何要使用设计模式

Demo代码

你可能感兴趣的:([设计模式笔记] No.8 模板方法模式(Template))