设计模式(三):模板方法模式
一、星巴克服务员
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.优点
- 模板方法模式通过抽象类,可以将代码的复用最大化
- 具体的算法只存在于模板中,让系统的修改变得容易,系统会比较有弹性
2.缺点
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。