前言
这一章的模板方法模式,个人感觉它是一个简单,并且实用的设计模式,先说说它的定义:
模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。(百度百科)
额, 这段定义呢,如果说我在不了解这个设计模式的时候,我看着反正是云里雾里的,毕竟定义嘛,就是用一堆看不懂的名词把一个看不懂的名词描述出来,但是学了这个设计模式,反过来看,又会觉得它的定义很正确。
模板方法模式的关键点有3个:
1,有一个由多个步骤构成的方法(模板方法)
2,子类可以自行实现其中的一个或多个步骤(模板方法中的步骤)
3,架构允许的情况下,子类可以重新定义某些步骤
话说,这不就是上面那段话吗?列成3点以后咋感觉越看越玄了呢?难道这就是传说中的玄学编程?
列出来的目的是,后面的例子里面会依次讲到这3点,话不多说,代码在idea里面已经蓄势待发!
模板方法模式基本实现
1,故事背景
在实现之前呢,需要有一个历史背景,不然不知道来龙去脉,容易印象不深刻,headfirst里面是这样的一个例子:
在一个店里面,有2种饮料,它们的冲泡步骤是这样的:
1,咖啡:把水煮沸,用沸水冲泡咖啡,倒进杯子,加糖和牛奶
2,茶:把水煮沸,用沸水浸泡茶叶,倒进杯子,加柠檬
当然,本着没有专业的自动化酿造技术的咖啡店不是一个好的科技公司,这段逻辑当然得用代码来实现了啊,然后就进入大家最喜欢的贴代码环节:
/** * 咖啡 */ public class Coffee { /** * 准备 */ public void prepare() { boilWater();//把水煮沸 brewCoffeeGrinds();//冲泡咖啡 pourInCup();//倒进杯子 addSugarAndMilk();//添加糖和牛奶 } /** * 把水煮沸 */ private void boilWater() { System.out.println("把水煮沸"); } /** * 冲泡咖啡 */ private void brewCoffeeGrinds() { System.out.println("用沸水冲泡咖啡"); } /** * 倒进杯子 */ private void pourInCup() { System.out.println("倒进杯子"); } /** * 添加糖和牛奶 */ private void addSugarAndMilk() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea { /** * 准备 */ public void prepare() { boilWater();//把水煮沸 steepTeaBag();//泡茶 pourInCup();//倒进杯子 addLemon();//加柠檬 } /** * 把水煮沸 */ private void boilWater() { System.out.println("把水煮沸"); } /** * 泡茶 */ private void steepTeaBag() { System.out.println("用沸水浸泡茶叶"); } /** * 倒进杯子 */ private void pourInCup() { System.out.println("倒进杯子"); } /** * 加柠檬 */ private void addLemon() { System.out.println("添加柠檬"); } }
上面贴了咖啡和茶的实现,对外提供的public方法是prepare()方法,其他的内部方法,都是private(不需要的方法不要提供出去,外面的世界锅太多,它们还小,经不住那么多的打击),按道理来说,上面两段代码,思路清晰,注释完整,代码整洁。
但是,boilWater(),pourInCup()两个方法,其实内容是一模一样的,对于一个程序来说,有2段一模一样的代码的时候,就应该思考,是不是有什么地方不对。因为,有2段就表示要改2个同样的地方,有10段,就要改10个同样的地方,System.out.println()当然能改啊。
2,逻辑抽象第一版
/** * 咖啡因的饮料(将烧水和倒进杯子两个方法抽象出来) */ public abstract class CaffeineBeverage { public abstract void prepare();//子类必须要有一个准备饮料的方法 /** * 把水煮沸 */ protected void boilWater() { System.out.println("把水煮沸"); } /** * 倒进杯子 */ protected void pourInCup() { System.out.println("倒进杯子"); } }
咖啡和茶的实现就会变成下面这样:
/** * 咖啡 */ public class Coffee extends CaffeineBeverage{ /** * 准备 */ public void prepare() { boilWater();//把水煮沸 brewCoffeeGrinds();//冲泡咖啡 pourInCup();//倒进杯子 addSugarAndMilk();//添加糖和牛奶 } /** * 冲泡咖啡 */ private void brewCoffeeGrinds() { System.out.println("用沸水冲泡咖啡"); } /** * 添加糖和牛奶 */ private void addSugarAndMilk() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea extends CaffeineBeverage{ /** * 准备 */ public void prepare() { boilWater();//把水煮沸 steepTeaBag();//泡茶 pourInCup();//倒进杯子 addLemon();//加柠檬 } /** * 泡茶 */ private void steepTeaBag() { System.out.println("用沸水浸泡茶叶"); } /** * 加柠檬 */ private void addLemon() { System.out.println("添加柠檬"); } }
比如在这个例子中,prepare()方法中的步骤还可以总结,抽象。
原来的冲泡步骤:
1,咖啡:把水煮沸,用沸水冲泡咖啡,倒进杯子,加糖和牛奶
2,茶:把水煮沸,用沸水浸泡茶叶,倒进杯子,加柠檬
抽象后:
咖啡/茶:把水煮沸,用沸水 【冲泡咖啡/浸泡茶叶】,倒进杯子,加 【糖和牛奶/柠檬】
第2步和第4步,还可以抽象成,冲泡,加调味品
3,模板方法模式抽象
那么抽象类的代码就会变成这样:
/** * 咖啡因的饮料(模板方法模式) */ public abstract class CaffeineBeverage { /** * 准备(构造成final方法,防止子类重写算法) */ final void prepare() { boilWater(); brew(); pourInCup(); addCondiments(); } /** * 冲泡 */ abstract void brew(); /** * 添加调料 */ abstract void addCondiments(); /** * 把水煮沸 */ public void boilWater() { System.out.println("把水煮沸"); } /** * 倒进杯子 */ public void pourInCup() { System.out.println("倒进杯子"); } }
咖啡和茶的实现如下:
/** * 咖啡 */ public class Coffee extends CaffeineBeverage { /** * 冲泡咖啡 */ void brew() { System.out.println("用沸水冲泡咖啡"); } /** * 添加糖和牛奶 */ void addCondiments() { System.out.println("添加糖和牛奶"); } } /** * 茶 */ public class Tea extends CaffeineBeverage { /** * 泡茶 */ void brew() { System.out.println("用沸水浸泡茶叶"); } /** * 加柠檬 */ void addCondiments() { System.out.println("添加柠檬"); } }
测试类:
/** * 测试类 */ public class Test { public static void main(String[] args) { Tea tea = new Tea(); Coffee coffee = new Coffee(); System.out.println("泡茶..."); tea.prepare(); System.out.println("冲咖啡..."); coffee.prepare(); } }
测试结果:
泡茶...
把水煮沸
用沸水浸泡茶叶
倒进杯子
添加柠檬
冲咖啡...
把水煮沸
用沸水冲泡咖啡
倒进杯子
添加糖和牛奶
这个就是一个模板方法模式比较通用的一个实现了,中间就有模板方法模式的2个要点:
(1),有一个由多个步骤构成的方法,这里就是prepare(),由4个步骤构成的一个模板方法
(2),子类可以自行实现其中的一个或多个步骤,咖啡和茶分别都实现了brew(),addCondiments()
其实到这个地方呢,模板方法模式的一般逻辑就大概讲完了,一般来说,实现也就是上面的那个样子,但是,还是有很多时候,会出现各种各样的其他实现方式,毕竟抽象这个东西,对于不同的业务,不同的逻辑,那简直就是多种多样,只要不违背设计原则,简单易用,那么总会有意识无意识的用到模板方法模式。
接下来就介绍几种常见的操作。
模板方法模式的常见操作
1,空实现
比如说,在把水煮沸前有一个前置步骤,有的饮料需要,但是有的饮料不需要,那么就可以在模板方法里面,给它留一个位置,让子类去选择性的覆盖实现
/** * 咖啡因的饮料(模板方法模式,空实现) */ public abstract class CaffeineBeverage { /** * 准备(构造成final方法,防止子类重写算法) */ final void prepare() { beforeBoilWater(); boilWater(); brew(); pourInCup(); addCondiments(); } /** * 烧水前置操作 */ protected void beforeBoilWater(){ } //其他方法省略... }
2,默认实现
比如说,加调味品这个步骤其实是可选的,加不加调味品,每种饮料加不加调味品,模板方法可以交给子类自己去控制。
/** * 咖啡因的饮料(模板方法模式,默认实现) */ public abstract class CaffeineBeverage { /** * 准备(构造成final方法,防止子类重写算法) */ final void prepare() { boilWater(); brew(); pourInCup(); if(needCondiments()){ addCondiments(); } } /** * 是否需要调味品 */ protected boolean needCondiments(){ return false; } //其他方法省略... }
总结
模板方法模式,其实就是,在抽象类中定义一个操作中的算法的步骤,而将一些步骤的实现延迟到子类中。
这里有一个建议是,尽量在抽象的时候,保证仅存在父类的方法去调用子类的方法,而不要同时存在子类的方法去调用父类的方法。
个人觉得,这样做的原因有几点:
1,贯彻这个建议后,整个逻辑会显得很简单易懂,反正没有实现的,在子类就能找到实现
2,相互依赖调来调去的后果就是在系统越来越复杂以后,最后就没人能看懂了
3,防止抽象类的方法变动引起子类的改动,这个其实不算原因,因为一般来说,抽象类是比较稳定的,而且,子类可以调的抽象类方法在修改时肯定要考量子类的,不允许子类调用的方法肯定都处理过了,一般肯定是调不到的(诶,反射你凑过来干嘛?)
最后的最后,总结一下模板方法模式的优缺点吧
优点:
1,代码复用便于维护,子类可扩展
2,行为由父类控制,子类只需要关心自己所需要的步骤即可,开发难度低
缺点:
1,每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大
所以,如果发现子类很多,是不是要想想是不是设计模式用错了,去隔壁找找其他的设计模式吧