zoukankan      html  css  js  c++  java
  • 设计模式(三):模板方法模式

    一、星巴克服务员

    1.初级服务员

    假如你是一位刚入职的星巴克服务员,负责为客人泡制咖啡和茶。公司规定茶和咖啡的泡制要遵循下面的步骤:

    于是你按照步骤单上的要求设计了咖啡(Coffee)和茶(Tea)并进行了制作

    *****************************Coffee*****************************

    /**
     * 咖啡
     * @author wuqi
     * @Date 2019/2/12 17:31
     */
    public class Coffee {
        /**
         *  准备咖啡
         */
        public void prepareRecipe(){
            boilWater();
            brewCoffeeGrinds();
            pourInCup();
            addSugarAndMilk();
        }
    
        /**
         * 将水煮沸
         */
        public void boilWater() {
            System.out.println("Boiling water");
        }
    
        /**
         * 用沸水冲泡咖啡
         */
        public void brewCoffeeGrinds(){
            System.out.println("Dripping Coffee through filter");
        }
    
        /**
         * 将咖啡倒进杯子里
         */
        public void pourInCup(){
            System.out.println("Pouring into cup");
        }
    
        /**
         * 加糖和牛奶
         */
        public void addSugarAndMilk(){
            System.out.println("Adding Sugar and Milk");
        }
    }

    *****************************Tea*****************************

    /**
     * 茶
     * @author wuqi
     * @Date 2019/2/12 17:36
     */
    public class Tea {
        /**
         * 准备茶
         */
        public void prepareRecipe(){
            boilWater();
            steepTeaBag();
            pourInCup();
            addLemon();
        }
    
        /**
         * 将水煮沸
         */
        public void boilWater() {
            System.out.println("Boiling water");
        }
    
        /**
         * 用沸水浸泡茶叶
         */
        public void steepTeaBag(){
            System.out.println("Steeping the tea");
        }
    
        /**
         * 将茶倒进杯子里
         */
        public void pourInCup(){
            System.out.println("Pouring into cup");
        }
    
        /**
         * 加柠檬
         */
        public void addLemon(){
            System.out.println("Adding Lemon");
        }
    }

    *****************************制作茶和咖啡*****************************

    /**
     * 没有使用模板方法模式测试
     * @author wuqi
     * @Date 2019/2/12 17:44
     */
    public class NoTemplateTest {
        public static void main(String[] args) {
            System.out.println("Making coffee...");
            Coffee coffee = new Coffee();
            coffee.prepareRecipe();
    
            System.out.println();
            System.out.println("Making Tea...");
            Tea tea = new Tea();
            tea.prepareRecipe();
        }
    }

    制作结果:

    你成功的将咖啡制作了出来,但是一个高级服务员从你身旁经过时看了一眼你的制作代码,嘲讽的说到:你这样制作的咖啡和茶有太多的重复代码,如果以后再让你制作另一种饮料,制作的步骤应该还是类似的,你还要再把重复的代码写一遍,最要命的是你竟然没有对这些饮料进行抽象。你虽然不高兴,但是别人把你设计的代码存在的问题一一指了出来,你还是比较感激的。你恭敬的说到:你说的对,我会改进一下的。

    2.高级服务员

    根据之前嘲讽你的高级服务员的指导,看着你之前所写的代码,你陷入了沉思:首先,我应该将咖啡和茶抽象出来,这两种饮料都含有咖啡因,就抽象成CaffeineBeverage吧。然后咖啡和茶有两个完全相同的方法boilWater和pourIntoCup,将这两个方法放在抽象类中。这还不够,对于prepareRecipe()来说,咖啡和茶都是遵循着相同的步骤,而且是定死的步骤,不允许我们修改,只是步骤具体实现的方式不一样,这些方式是由具体的类(咖啡和茶)决定的,所以我可以将这个方法放在抽象类中作为一个模板(template),并且不允许子类重写,子类只可以重写某些步骤具体的实现方式,就是怎么泡饮料,怎么加调料,这样的话我就需要将咖啡的brewCoffeeGrinds()和steepTeaBag()泛化成brew()这个名字,都是冲泡,让子类具体去决定怎么个泡法,而对于咖啡的addSugarAndMilk()和茶的addLemon()则可以泛化成addCondiments(),就是加调料,具体加什么调料也由子类去决定。按照这个思路你画出了相关的UML类图

    按照这个UML类图,你写下了改进后的代码:

    *****************************CaffeineBeverage*****************************

    /**
     * 咖啡因饮料
     * @author wuqi
     * @Date 2019/2/12 17:49
     */
    public abstract class CaffeineBeverage {
    
        /**
         * 抽象类定义模板,定义准备咖啡因饮料的具体步骤
         * 该步骤为final,不允许子类修改
         */
        public final void prepareRecipe(){
            boilWater();
            brew();
            pourInCup();
            addCondiment();
        }
    
        /**
         * 将水煮沸
         */
        public void boilWater() {
            System.out.println("Boiling water");
        }
    
        /**
         * 冲泡
         */
        public abstract void brew();
    
        /**
         * 将饮料倒进杯子里
         */
        public void pourInCup(){
            System.out.println("Pouring into cup");
        }
    
        /**
         * 加调料
         */
        public abstract void addCondiment();
    }

    *****************************Coffee*****************************

    /**
     * 咖啡
     * @author wuqi
     * @Date 2019/2/12 17:54
     */
    public class Coffee extends CaffeineBeverage {
        /**
         * 用水冲泡咖啡
         */
        @Override
        public void brew() {
            System.out.println("Dripping Coffee through filter");
        }
    
        /**
         * 添加糖和牛奶
         */
        @Override
        public void addCondiment() {
            System.out.println("Adding Sugar and Milk");
        }
    }

    *****************************Tea****************************

    /**
     * 茶
     * @author wuqi
     * @Date 2019/2/12 17:56
     */
    public class Tea extends CaffeineBeverage {
        /**
         * 用水浸泡茶叶
         */
        @Override
        public void brew() {
            System.out.println("Steeping the tea");
        }
    
        /**
         * 添加柠檬
         */
        @Override
        public void addCondiment() {
            System.out.println("Adding Lemon");
        }
    }

    *****************************测试制作咖啡和茶****************************

    /**
     * 模板方法测试
     * @author wuqi
     * @Date 2019/2/12 17:58
     */
    public class TemplateTest {
        public static void main(String[] args) {
            System.out.println("Making Coffee...");
            Coffee coffee = new Coffee();
            coffee.prepareRecipe();
    
            System.out.println();
            System.out.println("Making Tea...");
            Tea tea = new Tea();
            tea.prepareRecipe();
        }
    }

    运行结果:

    运行改进后的代码,我们依然成功制作了咖啡和茶。但是改进后的代码更加简洁了,复用度更高,而且以后如果再多出一个咖啡因饮料让我们去制作,我们只需要让新加的类继承CaffeineBeverage,并重写brew()和addCondiments方法即可,再也不用每次都添加很多重复的代码了,这样就使我们的系统非常的具有弹性。你非常的满意,看着改进后的代码,你突然想到这不就是之前我学习设计模式中的模板方法模式吗!想不到在这里用到了。这次,你老板从你身旁经过,看着你制作咖啡的代码,满意的点了点头,说到:不错,你这个咖啡制作的代码非常好,你用到了模板方法模式,现在开始,我正是聘用你为高级服务员了。你非常诧异也非常高兴的说了声:谢谢老板!随后你老板又说到:我们的茶后面不准备再加调料了,正好模板方法的钩子可以派上用场,你去改进一下吧,也当作你作为高级服务员的最后一道考核。你恭敬的回了句:是的!老板!

    3.神奇的钩子

    你努力的回忆着模板方法模式的知识,模板方法模式就是在抽象的基类中定义了一个模板,这个模板中规定了完成该方法所需的具体算法,对应于我们制作饮料的各个步骤,子类需要按照这个步骤去执行该方法,步骤中的一些实现方式可以延迟到子类去决定。而钩子是穿插在算法当中的一个可选的部分,它有默认的实现,钩子可以由子类实现并决定是否启用。你又画了个UML类图来表示加上钩子后的饮料制作方式。

    根据这个UML类图,你写下了加上钩子的代码

     *****************************CaffeineBeverage****************************

    /**
     * 咖啡因饮料
     * @author wuqi
     * @Date 2019/2/12 17:49
     */
    public abstract class CaffeineBeverage {
        /**
         * 抽象类定义模板,定义准备咖啡因饮料的具体步骤
         * 该步骤为final,不允许子类修改
         */
        public final void prepareRecipe(){
            boilWater();
            brew();
            pourInCup();
            if(needAddCondiments()){
                addCondiment();
            }
        }
    
        /**
         * 钩子
         * @return
         */
        protected boolean needAddCondiments(){
            return false;
        }
    
        /**
         * 将水煮沸
         */
        public void boilWater() {
            System.out.println("Boiling water");
        }
    
        /**
         * 冲泡
         */
        public abstract void brew();
    
        /**
         * 将饮料倒进杯子里
         */
        public void pourInCup(){
            System.out.println("Pouring into cup");
        }
    
        /**
         * 加调料
         */
        public abstract void addCondiment();
    }

     *****************************Coffee****************************

    /**
     * 咖啡
     * @author wuqi
     * @Date 2019/2/12 17:54
     */
    public class Coffee extends CaffeineBeverage {
        /**
         * 用水冲泡咖啡
         */
        @Override
        public void brew() {
            System.out.println("Dripping Coffee through filter");
        }
    
        /**
         * 添加糖和牛奶
         */
        @Override
        public void addCondiment() {
            System.out.println("Adding Sugar and Milk");
        }
    
        /**
         * 子类覆盖钩子
         * @return
         */
        @Override
        public boolean needAddCondiments() {
            return true;
        }
    
    }

    *****************************Tea****************************

    /**
     * 茶
     * @author wuqi
     * @Date 2019/2/12 17:56
     */
    public class Tea extends CaffeineBeverage {
        /**
         * 用水浸泡茶叶
         */
        @Override
        public void brew() {
            System.out.println("Steeping the tea");
        }
    
        /**
         * 添加柠檬
         */
        @Override
        public void addCondiment() {
            System.out.println("Adding Lemon");
        }
    }

    *****************************TemplateTest****************************

    /**
     * 模板方法测试
     * @author wuqi
     * @Date 2019/2/12 17:58
     */
    public class TemplateTest {
        public static void main(String[] args) {
            System.out.println("Making Coffee...");
            Coffee coffee = new Coffee();
            coffee.prepareRecipe();
    
            System.out.println();
            System.out.println("Making Tea...");
            Tea tea = new Tea();
            tea.prepareRecipe();
        }
    }

    测试结果:

    可以看到现在制作茶已经不会再添加柠檬了,因为模板中钩子默认的是不加调料的,而Tea中没有重写钩子,所以茶现在是不加调料了。你把改进后的代码交给老板看,你老板非常满意的点了点头。从此,你开心的做起了一名高级服务员。

    二、定义模板方法

    1.模板方法的概念

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

    2.UML类图

     3.模板方法模式中存在的OO设计原则

    1.好莱坞原则

    模板方法模式中用到OO设计原则中的好莱坞原则,别调用(打电话给)我们,我们会(打电话给)调用你。

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

    2.模板方法模式和好莱坞原则

    模板方法模式就是在抽象基类中定义了一个模板,规定了子类执行的具体步骤和时序,子类只是决定某些步骤的实现方式。抽象基类是高层,子类是低层,这刚好符合好莱坞原则。

    三、模板方法模式的应用场景

    模板方法模式适用需要定义一套固定的模板,模板中的算法是不更改的,但是某些具体的实现方式可以由子类决定的场景

    四、模板方法模式的优缺点

    1.优点

    1. 模板方法模式通过抽象类,可以将代码的复用最大化
    2. 具体的算法只存在于模板中,让系统的修改变得容易,系统会比较有弹性

    2.缺点

           需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。

  • 相关阅读:
    2021 CCPC 桂林站 补题
    2021 ICPC 上海 流水账
    2021 ICPC 沈阳 补题
    vi中的多行注释和取消注释
    查询列表可筛选可模糊查询的写法
    mybatisplus逻辑删除deleted
    @JsonFormat
    @Component类相互引用的加载顺序
    Chrome浏览器嗅探方法
    DataAdapter.FillSchema 方法
  • 原文地址:https://www.cnblogs.com/wutianqi/p/10366487.html
Copyright © 2011-2022 走看看