今天不学习,明天变辣鸡
定义
模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
结构图及含义
类图符号意义
+:表示public-
-:表示privat
AbstractClass(抽象类):其实也就是一抽象模板,定义并实现了一个模板方法( templateMethod() )。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
ConcreteClass(子类),实现父类所定义的一个或多个抽象方法。每一个AbstractClass都可以有任意多个ConcreteClass与之对应,而每一个ConcreteClass都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
特点:模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势。模板方法模式就是提供了一个很好的代码复用平台,当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
先举一个简单的例子
创建一个抽象模板结构(AblstractClass)
/**
*@description 模板方法
*@author Saiuna
*@date 2020/7/6 1:25
*/
public abstract class AbstractClass {
//模板方法用来控制子类的顺序 , 声明final不让子类覆盖这个方法,防止改变执行顺序
public final void human(){
sport();
learn();
life();
}
void sport(){
System.out.println("我要跑步来锻炼身体");
}
void learn(){
System.out.println("九年义务教育");
}
//生活就像海洋
abstract void life();
}
具体模板(ConcreteClass)
/**
*@description
*@author Saiuna
*@date 2020/7/6 1:27
*/
public class ConcreteClass extends AbstractClass {
@Override
public void sport() {
System.out.println("我通过打篮球来锻炼身体");
}
//生活就像海洋
@Override
public void life() {
System.out.println("生活就像海洋,只有意志坚强的人才能到达彼岸");
}
public static void main(String[] args) {
new ConcreteClass().human();
}
}
输出
我通过打篮球来锻炼身体
九年义务教育
生活就像海洋,只有意志坚强的人才能到达彼岸
以上是模板方法模式的一个最基本应用,下面再深入一点,介绍一下“钩子”
钩子
从代码层面展示抽象类的定义
abstract class AbstractClass {
// 这就是模板方法。它被声明为final,以免子类改变这个算法的顺序
final void templateMethod() {
// 模板方法定义了一连串的步骤,每个步骤由一个方法代表
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation() {
// 这里是实现
}
// 这是一个具体的方法,但他什么都不做。我们叫它为hook(钩子),马上就来揭晓它如何使用
void hook();
}
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类决定。
钩子有很多用途,我们先看其中一个:
咖啡
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
茶
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶倒进杯子
- 加柠檬
第1和第3步其实是一样的,第2和第4步的操作一样只是内容不同。据此可以抽取出一个共同点,设计出一个抽象类:
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 我们加上一个小小的条件语句,该条件是否成立,是由一个具体方法决定
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
// 这就是一个钩子,子类可以覆盖这个方法,但不一定需要使用
boolean customerWantsCondiments() {
return true;
}
}
为了使用钩子,我们在子类中覆盖它。在这里,钩子控制了咖啡因饮料是否执行某部分算法;比如,饮料中是否需要加进调料
咖啡
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
public boolean customerWantsCondiments() {
// 询问用户,是否要加调料
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("Would you like milk and sugar with your coffee (y/n)? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
茶类
/**
*
*@author Saiuna
*@date 2020/7/6 8:47
*/
public class TeaWithHook extends CaffeineBeverageWithHook {
@Override
public void brew() {
System.out.println("Steeping the tea");
}
@Override
public void addCondiments() {
System.out.println("Adding Lemon");
}
@Override
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
// get the user's response
String answer = null;
System.out.print("Would you like lemon with your tea (y/n)? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
上面就是一个钩子,询问顾客是否想要调料?如果需要,就执行内容,否则就不执行。测试代码如下:
public class BeverageTestDrive {
public static void main(String[] args) {
TeaWithHook teaHook = new TeaWithHook();
CoffeeWithHook coffeeHook = new CoffeeWithHook();
System.out.println("\nMaking tea...");
teaHook.prepareRecipe();
System.out.println("\nMaking coffee...");
coffeeHook.prepareRecipe();
}
}
执行结果如下:茶要加调料就给你加上了;咖啡不需要加调料,就没给你加
Making tea...
Boiling water
Steeping the tea
Pouring into cup
Would you like lemon with your tea (y/n)? y
Adding Lemon
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into cup
Would you like milk and sugar with your coffee (y/n)? n
Process finished with exit code 0
那么,我们使用钩子的真正目的是什么呢?
钩子有几种用法。如我们之前所说的,钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理。钩子的另一个用法,是让子类能够 有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应;就像上述的饮料是否要加调料一样。
好莱坞原则:“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。
好莱坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法时,我们告诉子类“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:
我们之前还知道一个原则叫依赖倒置原则,好莱坞原则也是有点这个味道的对吧。他们之间的关系是如何的呢?
依赖倒置原则教我们尽量避免使用具体类,而多实用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是以来倒置原则更加注重如何在设计中避免依赖。
好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过于依赖它们。
注: 部分段落源于其他文章,原出处找不到了~