名称
模板方法模式(TEMPLATE METHOD)
目的
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
适用性
模板方法应用于下列情况:
一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作,最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
控制子类扩展。模板方法只在特定点调用“hook”操作(参见效果一节),这样就只允许在这些点进行扩展。
结构
抽象类(AbstractClass):
定义抽象的原语操作(primitive operation),具体的子类将重定义它们以实现一个算法的各步骤。
注意:这里的原语操作并不是只计算机系统层面的原语操作(不可分割的操作),而是抽象类或接口中定义的方法,这些方法的具体实现细节留给具体的子类去完成。
实现一个模板方法,定义一个算法的骨架。该模板方法不仅调用原语操作,也调用定义在AbstractClass或其他对象中的操作。
具体类(ConcreteClass):实现原语操作以完成算法中与特定子类相关的步骤。
协作
ConcreteClass靠AbstractClass来实现算法中不变的步骤。
效果
优点
封装了不变的部分,提高了代码复用性。模板方法模式通过在抽象类中提供一个算法框架,允许子类在不改变算法结构的前提下,通过重定义某些步骤的方式重用该算法。模板方法是一种代码复用的基本技术,它在类库中尤为重要,提取了类库中的公共行为。它提供了一种反向的控制结构,这种结构有时被称为“好莱坞法则”,即“别找我们,我们找你”。这指的是一个父类调用一个子类的操作,而不是相反。
提高了扩展性。子类可以通过覆盖父类中的抽象方法来实现算法的某些步骤,从而达到扩展算法的目的,而不需要修改原有代码。
符合开闭原则。通过父类调用子类的操作,即对扩展开放,又对修改关闭。
提高代码的可维护性。由于行为由父类控制,子类实现,因此当算法的结构发生变化时,只需要修改父类即可,减少了子类的变化。
缺点
增加子类数量。每实现一个不同的需求就会增加一个子类,这样会导致类的数量增加,增加了系统的复杂度。
过度约束子类。如果父类中的模板方法定义得过于严格,可能会限制子类的行为,使得子类难以适应新的需求。
钩子方法的使用。有时为了给子类更多的控制权,父类中会定义很多钩子方法,这可能会使父类变得庞大且难以理解。
强制继承。模板方法模式依赖于继承,如果继承层次结构设计不合理,可能会导致系统难以维护。
实现
访问控制。在C++中,一个模板方法调用的原语操作可以被定义为保护成员,这保证它们只被模板方法调用(java中做不到只让父类访问子类方法这一点)。必须重定义的原语操作须定义为纯虚函数。模板方法自身不需被重定义,因此可以将模板方法定义为一个非虚成员函数。
尽量减少原语操作。定义模板方法的一个重要目的是尽量减少一个子类具体实现该算法时必须重定义的那些原语操作的数目。需要重定义的操作越多,客户程序就越冗长。
命名约定。可以给需要被重定义的那些操作的名字加上一个前缀以识别它们,比如do、should等。
在抽象类中定义模板方法并且保证模板方法不能被重定义。在java中可以使用final保证子类无法重写模板方法。
确定模板方法调用的操作。模板方法能调用下列类型的操作:
具体的操作(ConcreteClass或对客户类的操作)。
具体的AbstractClass的操作(即公共方法,通常对子类有用的操作,或者说提取出来的公共部分操作)。
原语操作(即抽象操作)。
工厂方法模式下的工厂方法(Factory Method)
钩子操作(hookoperations),它提供了缺省的行为,子类可以在必要时进行扩展。一个钩子操作在缺省操作通常是一个空操作。
注意:很重要的一点是模板方法应该指明哪些操作是钩子操作(可以被重定义),哪些是抽象操作(必须被重定义)。要有效地重用一个抽象类,子类编写者必须明确了解哪些操作是设计为有待重定义的。抽象类中存在多种方法,它们分别是:模板方法、抽象方法、钩子方法、公共方法(供子类调用的)。
应用
问题
有些人喜欢喝茶,有些人喜欢喝咖啡,它们在制作过程中有相同的步骤,比如说都要烧开水,我们可以把共同的地方提取出来,使用模板方法模拟他们的制作过程。把它们的制作过程简化一下,分别是烧开水、冲泡、倒入杯中、加料,其中相同烧开水和导入杯中可以提取出来作为公共方法,不相同的冲泡、加料在抽象类中作为抽象方法,交给子类自定义。
扩展:像加料这一步,喝茶的人一般是不需要加的,所以可以定义一个钩子方法去询问顾客加不加料,喝茶通常是不加料就不去询问,喝咖啡的人加料不加料的概率都差不多,所以咖啡具体类中重写该方法进行询问,根据答复执行加料或不加料。
示例1
UML
代码示例
抽象类:咖啡因饮料
package com.ysj.part5.templateMethod.normal.abstractClass;
/**
* 抽象类:咖啡因饮料
*/
public abstract class CaffeineBeverage {
/**
* 模板方法,用来控制泡茶与冲咖啡的流程
* 申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
*/
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 沏(茶)/冲(咖啡)
* 定义为抽象方法,由子类实现
*/
public abstract void brew();
/**
* 加料
* 定义为抽象方法,由子类实现
*/
public abstract void addCondiments();
/**
* 烧开水
*/
void boilWater() {
System.out.println("烧开水...");
}
/**
* 倒入杯中
*/
void pourInCup() {
System.out.println("导入杯中...");
}
}
具体类:茶、咖啡
package com.ysj.part5.templateMethod.normal.concreteClass;
import com.ysj.part5.templateMethod.normal.abstractClass.CaffeineBeverage;
/**
* 具体实现类:茶
*/
public class Tea extends CaffeineBeverage {
public void addCondiments() {
System.out.println("加柠檬...");
}
public void brew() {
System.out.println("泡茶...");
}
}
package com.ysj.part5.templateMethod.normal.concreteClass;
import com.ysj.part5.templateMethod.normal.abstractClass.CaffeineBeverage;
/**
* 具体实现类:咖啡
*/
public class Coffee extends CaffeineBeverage {
public void addCondiments() {
System.out.println("加糖或者牛奶...");
}
public void brew() {
System.out.println("过滤嘴滴咖啡...");
}
}
客户端:机器人
package com.ysj.part5.templateMethod.normal;
import com.ysj.part5.templateMethod.normal.abstractClass.CaffeineBeverage;
import com.ysj.part5.templateMethod.normal.concreteClass.Coffee;
import com.ysj.part5.templateMethod.normal.concreteClass.Tea;
/**
* 模板方法模式
* 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
* 示意图:/doc/diagram/模板方法示意图*.jpg
*/
public class Client {
public static void main(String[] args) {
//泡茶
System.out.println("========开始泡茶========");
Tea tea = new Tea();
tea.prepareRecipe();
//泡咖啡
System.out.println("========开始泡咖啡========");
Coffee coffee = new Coffee();
coffee.prepareRecipe();
}
}
示例2(扩展钩子)
UML
代码示例
抽象类:咖啡因饮料
package com.ysj.part5.templateMethod.hook.abstractClass;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 抽象类:咖啡因饮料(带钩子方法)
*/
public abstract class CaffeineBeverageWithHook {
/**
* 模板方法,用来控制泡茶与冲咖啡的流程
* 申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
*/
public void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 如果顾客需要添加调料,才会调用addCondiments()方法
if (customerWantsCondiments()) {
addCondiments();
}
}
/**
* 沏(茶)/冲(咖啡)
* 定义为抽象方法,由子类实现
*/
public abstract void brew();
/**
* 加料
* 定义为抽象方法,由子类实现
*/
public abstract void addCondiments();
/**
* 烧开水
*/
void boilWater() {
System.out.println("烧开水...");
}
/**
* 倒入杯中
*/
void pourInCup() {
System.out.println("倒入杯中...");
}
/**
* 顾客是否需要调味品,钩子方法,默认返回true,子类可以覆盖这个方法
* @return
*/
public boolean customerWantsCondiments() {
return false;
}
/**
* 获取用户输入,提取为公共方法,让子类钩子函数可调用
* @param notion 提示内容
* @return 用户输入的字符串
*/
public String getUserInput(String notion) {
String answer = null;
System.out.print(notion);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
if (answer == null) {
return "n";
}
return answer;
}
}
具体类:茶、咖啡
package com.ysj.part5.templateMethod.hook.concreteClass;
import com.ysj.part5.templateMethod.hook.abstractClass.CaffeineBeverageWithHook;
/**
* 具体实现类:茶
*/
public class TeaWithHook extends CaffeineBeverageWithHook {
public void addCondiments() {
System.out.println("加柠檬...");
}
public void brew() {
System.out.println("泡茶...");
}
}
package com.ysj.part5.templateMethod.hook.concreteClass;
import com.ysj.part5.templateMethod.hook.abstractClass.CaffeineBeverageWithHook;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 具体实现类:咖啡
*/
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void addCondiments() {
System.out.println("加糖或者牛奶...");
}
public void brew() {
System.out.println("过滤嘴滴咖啡...");
}
/**
* 覆盖该钩子,提供自己的实现方法
*/
public boolean customerWantsCondiments() {
if ("y".equals(getUserInput("你想要加牛奶或者糖吗?(y/n):").toLowerCase())) {
return true;
} else {
return false;
}
}
}
客户端:机器人
package com.ysj.part5.templateMethod.hook;
import com.ysj.part5.templateMethod.hook.concreteClass.CoffeeWithHook;
import com.ysj.part5.templateMethod.hook.concreteClass.TeaWithHook;
/**
* 使用钩子实现泡茶喝咖啡时询问是否需要调味料
* 所谓钩子就是一种被声明在抽象类中的方法,但只有空的或者默认的实现。
* 钩子的存在可以使子类能够对算法的不同点进行挂钩,即让子类能够对模
* 板方法中某些即将发生变化的步骤做出相应的反应。当然要不要挂钩,由子类决定。
*/
public class Client {
public static void main(String[] args) {
//泡茶
System.out.println("========开始泡茶========");
TeaWithHook teaWithHook = new TeaWithHook(); //此处茶没有重写customerWantsCondiments方法,所以调用的是父类的
teaWithHook.prepareRecipe();
//泡咖啡
System.out.println("========开始泡咖啡========");
CoffeeWithHook coffeeHook = new CoffeeWithHook();
coffeeHook.prepareRecipe();
}
}
已知应用
Spring Framework
Spring的JdbcTemplate 是 Spring 中用于简化 JDBC 操作的一个类,它使用了模板方法模式。JdbcTemplate 定义了一些固定的操作步骤,子类或用户可以通过实现特定的方法来扩展或覆盖这些步骤。
评论区