什么是模板方法模式?
定义一个操作中算法的骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
好抽象的概念啊,文绉绉的东西就是不讨人喜欢,下面我用一个生活中常见的例子来举例说明吧
上图是个饮料机,大家都很熟悉吧,各大商场随处可见的一个东西,这里举个例子,比如我们想喝某种饮料,只要按下对应的饮料类别,饮料就自动出来了。
这里我们可以抽象化的想象下饮料在机器里的制作过程(这里只是简单举例,别钻牛角尖哈)
大致我们可以分成4个步骤
①烧水 ②冲泡饮料 ③把饮料倒入杯中 ④加入调料
例如:
咖啡:烧开热水-->加入咖啡粉冲泡-->把饮料倒入杯中-->加入少许糖
奶茶:烧开热水-->加入奶茶粉冲泡-->把饮料加入杯中-->加入椰果/珍珠
不难发现,饮料制作过程中的步骤中的①烧水、③把饮料倒入杯中是重复工作,制泡哪种饮料都一样,那么也就是重复工作,我们可以把它设定为通用性操作。
我们只需要去关心步骤②和步骤④即可
由于制泡饮料的步骤就是这4步,所以我们可以把它抽象成一个"制作饮料模板"出来,下面就以上面这个例子,我用代码来说明
DrinkTemplate.java(模板类)
这是一个制作饮料的模板类,也就是制作所有饮料的基类
我们可以把这4个步骤封装到一个模板方法里,并实现里面的通用步骤
由于避免继承它的子类去修改整体制作架构,所以这个方法用了final修饰符来修饰,好比著名的"好莱坞原则":Don't call us, we'll call you 子类需要听从父类的安排
由于步骤②和步骤④需要根据具体制泡的饮料来确定,所以需要延迟到子类去实现,这里采用了protected修饰符以便子类可以复写,其他方法就可以直接写"死"掉,用private修饰符修饰,这样的使得代码工作人员能够更加关注自身的工作,而不必去考虑一些其他因素。
1 packagecom.lcw.template.test;2
3 public abstract classDrinkTemplate {4
5 /**抽象基类6 *7 * 制作饮料方法模板8 * 4个步骤 1、烧水 2、冲泡饮料 3、把饮料倒入杯中 4、加调料9 * 由于步骤1、3是通用的步骤,适合于制作任何饮料,所以可以把它写死10 * 2和4步骤,针对不同的饮料有不同的选择,所以可以把它延迟到子类去复写实现(注意访问修饰符)11 */
12 public final voiddrinkTempLate(){13 boilWater();//烧水
14 brew();//冲泡饮料
15 pourInCup();//把饮料倒入杯中
16 addCondiments();//加调料
17 }18
19 protected abstract void addCondiments();//加调料,由于饮料所加调料各不相同,所以可以延迟到子类实现
20
21 private voidpourInCup() {22 System.out.println("把饮料倒入杯中...");23 }24
25 protected abstract void brew();//冲泡饮料 ,由于饮料所用的材料各不相同,所以可以延迟到子类实现
26
27 private voidboilWater() {28 System.out.println("烧水步骤进行中...");29 }30 }
MakeCoffee.java(冲泡咖啡类)
这个没啥好说的,就是继承了抽象基类,并复写了它的抽象方法
1 packagecom.lcw.template.test;2 /**
3 *4 *@authorBalla_兔子5 * 冲泡咖啡6 *7 */
8 public class MakeCoffee extendsDrinkTemplate {9
10 @Override11 protected voidaddCondiments() {12 System.out.println("加糖...");13 }14
15 @Override16 protected voidbrew() {17 System.out.println("加入咖啡粉冲泡...");18 }19
20 }
MakeMilkTea.java(冲泡奶茶类)
1 packagecom.lcw.template.test;2 /**
3 * 冲泡奶茶4 *@authorBalla_兔子5 *6 */
7 public class MakeMilkTea extendsDrinkTemplate {8
9 @Override10 protected voidaddCondiments() {11 System.out.println("加椰果...");12 }13
14 @Override15 protected voidbrew() {16 System.out.println("加入奶茶粉冲泡...");17 }18
19 }
Test.java(测试类)
1 packagecom.lcw.template.test;2
3 public classTest {4
5 /**
6 *@authorBalla_兔子7 */
8 public static voidmain(String[] args) {9 DrinkTemplate coffee=newMakeCoffee();10 coffee.drinkTempLate();11 System.out.println("*******************************");12 DrinkTemplate milkTea=newMakeMilkTea();13 milkTea.drinkTempLate();14 }15
16 }
看下运行效果:
哈哈,这样的实现类写起来是不是很清晰明了啊,只需要去复写我们需要关心的方法即可,大大提高了代码的复用性。
但这里有个问题就暴露出来了,冲泡咖啡的实现固然没错,但总有些人喝咖啡是不加糖的,这是该怎么办呢?
这里就引入了一个"钩子"hook概念
我们可以在某个具体实现方法前后分别加入钩子,就好比是前置方法或者后置方法,就像日志技术一样,在每完成一个业务动作前都需要记录日志
而这个前置方法,我们可以利用一个布尔来做判断,并给它一个默认,来看看具体实现方法
DrinkTemplate.java(模板类)
1 packagecom.lcw.template.test;2
3 public abstract classDrinkTemplate {4
5 /**抽象基类6 *7 * 制作饮料方法模板8 * 4个步骤 1、烧水 2、冲泡饮料 3、把饮料倒入杯中 4、加调料9 * 由于步骤1、3是通用的步骤,适合于制作任何饮料,所以可以把它写死10 * 2和4步骤,针对不同的饮料有不同的选择,所以可以把它延迟到子类去复写实现(注意访问修饰符)11 */
12 public final voiddrinkTempLate(){13 boilWater();//烧水
14 brew();//冲泡饮料
15 pourInCup();//把饮料倒入杯中
16 if(condition()==true){//若条件允许,则加入调料,默认允许
17 addCondiments();//加调料
18 }19 }20
21 protected booleancondition() {22 return true;23 }24
25 protected abstract void addCondiments();//加调料,由于饮料所加调料各不相同,所以可以延迟到子类实现
26
27 private voidpourInCup() {28 System.out.println("把饮料倒入杯中...");29 }30
31 protected abstract void brew();//冲泡饮料 ,由于饮料所用的材料各不相同,所以可以延迟到子类实现
32
33 private voidboilWater() {34 System.out.println("烧水步骤进行中...");35 }36 }
Test.java(测试类)
1 packagecom.lcw.template.test;2
3 public classTest {4
5 /**
6 *@authorBalla_兔子7 */
8 public static voidmain(String[] args) {9 DrinkTemplate coffee=newMakeCoffee();10 coffee.drinkTempLate();11 System.out.println("咖啡制作完毕!");12 System.out.println("*******************************");13 DrinkTemplate milkTea=newMakeMilkTea();14 milkTea.drinkTempLate();15 System.out.println("奶茶制作完毕!");16 }17
18 }
看下这次的效果,哈哈,无糖咖啡出炉~
总结下:
先说说模板方法模式的优点:
1、封装性好 2、复用性好、 3、屏蔽细节 4、便于维护
至于缺点呢,就是继承问题,在JAVA里只能继承一个父类。
流程: 分析场景-->步骤抽取-->重构代码-->重要、复杂的算法,核心算法设计为模版
注意点:
模版方法需要声明成 public final
private方法是基本逻辑
protect abstract 方法是可扩展方法
钩子使得模板方法更加灵活