模板方法模式也是也是比较容易理解的,就比如说做饭,同样的步骤不同的人做味道是不一样的。或者是造汽车,同样的步骤,造车厂商不一样,造出来的汽车质量不同。这就是模板方法模式。这篇文章将通过案例详细的讲解一下模板方法模式。
一、模板方法模式的定义
在软件开发中,某个方法的实现需要多个步骤,其中有些步骤是固定的,而有些步骤并不固定,存在可变性。为了提高代码的复用性和系统的灵活性,可以使用 模板方法模式 的设计模式对这类情况进行设计。
在模板方法模式中将实现功能的每一步骤所对应的方法称为 基本方法,而将调用这些基本方法同时定义基本方法的执行次序的方法称为 模板方法。
定义: 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
模板方法模式将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称为模板方法的方法的定义这些基本方法的执行次序,并且可以通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。
二、模板方法模式的结构
模板方法模式包含以下重要角色:
抽象类 : 负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
模板方法 : 定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法 : 是实现算法中各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法 : 一个抽象方法由抽象类声明、由其具体子类实现。
具体方法 : 一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法 : 在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为 isXxx,返回值类型为 boolean 类型。
具体子类 : 实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
三、模板方法模式的实现
需求案例:去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、离开银行等操作。
我们可以发现,在这个流程中取号、排队和对离开银行的操作对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
现在使用模板方法模式来实现上述案例,类图如下:
具体的类设计如下:
//抽象类,定义银行业务算法骨架
public abstract class BankBusiness {
final void work(){
//1.取号
getNum();
//2.排队
doWait();
//3.办理具体业务
doService();
//4.离开银行
doExit();
}
public void getNum(){
System.out.println("取号");
}
public void doWait(){
System.out.println("排队");
}
public abstract void doService();
public void doExit(){
System.out.println("离开银行");
}
}
存款业务实现类:
public class SaveMoney extends BankBusiness{
@Override
public void doService() {
System.out.println("存款业务");
}
}
取款业务实现类:
public class TakeMoney extends AbstractClass {
public void doService() {
System.out.println("办理取款业务");
}
}
客户端类:
public class client {
public static void main(String[] args) {
SaveMoney saveMoney = new SaveMoney();
saveMoney.work();
System.out.println("-----------------------------");
TakeMoney takeMoney = new TakeMoney();
takeMoney.work();
}
}
执行结果:
这里要注意一点就是work方法我们一般会用final修饰,防止被子类重写这个方法,因为整体的算法骨架是不变的,只是变其中的几个步骤进行子类自定义扩展化。
四、模板方法模式的扩展
在上述的案例中,在现实生活中像取号和排队操作是不一定需要执行,比如一般在一般中如果是VIP的话那去办理业务一般是有独立的窗口,不需要去取号和排队的,因此,VIP的用户只需执行办理具体业务和离开银行即可。
我们可以引入模板方法模式中的 钩子方法,通过钩子方法返回的布尔值来决定是否需要执行模板方法中的某个操作。
钩子方法 的作用主要体现在以下几个方面:
提供默认行为: 钩子方法通常在抽象类中提供了一个默认的实现,这个默认实现可以在模板方法中被调用。如果具体子类选择不覆盖钩子方法,就会使用这个默认行为。这使得在不同的子类中可以有一些通用的行为。
允许选择性的行为扩展: 具体子类可以选择性地覆盖钩子方法,以添加或修改某些行为,以适应特定的需求。这为子类提供了一定的灵活性,使其能够自定义模板方法的部分行为,而不需要修改整个模板方法。
控制模板方法执行流程: 钩子方法的返回值通常被用于在模板方法中控制流程。例如,可以使用钩子方法返回的布尔值来决定是否执行模板方法中的某个步骤。这样,通过覆盖钩子方法,可以在不同的子类中控制模板方法的执行流程。
接下来,我们使用钩子方法对上述例子进行改造,具体实现类如下:
抽象类:
//抽象类,定义银行业务算法骨架
public abstract class BankBusiness {
final void work(){
if (!isVIP()) {
getNum();
doWait();
}
// //1.取号
// getNum();
// //2.排队
// doWait();
//3.办理具体业务
doService();
//4.离开银行
doExit();
}
public boolean isVIP(){
return false;
}
public void getNum(){
System.out.println("取号");
}
public void doWait(){
System.out.println("排队");
}
public abstract void doService();
public void doExit(){
System.out.println("离开银行");
}
}
存款业务类:
public class SaveMoney extends BankBusiness{
@Override
public void doService() {
System.out.println("存款业务");
}
@Override
public boolean isVIP() {
return true;
}
}
取款业务类:
public class TakeMoney extends BankBusiness{
@Override
public void doService() {
System.out.println("取款业务");
}
}
客户端:
public class client {
public static void main(String[] args) {
SaveMoney saveMoney = new SaveMoney();
saveMoney.work();
System.out.println("-----------------------------");
TakeMoney takeMoney = new TakeMoney();
takeMoney.work();
}
}
可以看到vip用户不用取号排队 ,直接进行业务办理
五、模板方法模式的优缺点
优点:
提高代码服复用性 : 将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
实现了反向控制 : 通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
六、模板方法模式的使用场景
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。