一、简介
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
二、优缺点
优点
- 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
- 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
- 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
- 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
缺点
- 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。此时,可以结合桥接模式来进行设计。
三、应用场景
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
四、模式结构
模板方法模式包含以下角色:
- AbstractClass(抽象类):在抽象类中定义了一系列基本操作 (PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法,用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
- ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。
五、实战
具体实现
定义 AbstractClass 抽象类
class AbstractClass {
public method1(): void {
throw new Error("Abstract Method");
}
public method2(): void {
throw new Error("Abstract Method");
}
public method3(): void {
throw new Error("Abstract Method");
}
public templateMethod(): void {
console.log("templateMethod is being called");
this.method1();
this.method2();
this.method3();
}
}
定义 ConcreteClass 类
class ConcreteClass extends AbstractClass {
public method1(): void {
console.log("method1 of ConcreteClass");
}
public method2(): void {
console.log("method2 of ConcreteClass");
}
public method3(): void {
console.log("method3 of ConcreteClass");
}
}
使用示例
function show(): void {
const cc: ConcreteClass = new ConcreteClass();
cc.templateMethod();
}
以上代码成功运行后会输出以下结果:
templateMethod is being called
method1 of ConcreteClass
method2 of ConcreteClass
method3 of ConcreteClass
下面为了让小伙伴们能更好地理解模板方法设计模式,我们来以 “泡咖啡与泡茶” 这个经典的例子来深入介绍模板方法。
首先,我们先来泡一杯咖啡,泡咖啡的步骤通常如下:
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
接下来,开始准备我们的茶,泡茶的步骤跟泡咖啡的步骤类似:
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶水倒进杯子
- 加柠檬
根据上面泡咖啡和泡茶的步骤,我们可以整理出以下表格:
泡咖啡步骤 | 泡茶步骤 |
---|---|
把水煮沸 | 把水煮沸 |
用沸水冲泡咖啡 | 用沸水浸泡茶叶 |
把咖啡倒进杯子 | 把茶水倒进杯子 |
加糖和牛奶 | 加柠檬 |
我们可以发现泡咖啡和泡茶主要有以下不同点:
- 原料不同。一个是咖啡,一个是茶,但我们可以把它们都抽象为 “饮料”。
- 泡的方式不同。咖啡是冲泡,而茶叶是浸泡,我们可以把它们都抽象为 “泡”。
- 加入的调料不同。一个是糖和牛奶,一个是柠檬,但我们可以把它们都抽象为 “调料”。
经过抽象之后,不管是泡咖啡还是泡茶,我们都能整理为下面四步:
- 把水煮沸
- 用沸水冲泡饮料
- 把饮料倒进杯子
- 加调料
所以,不管是冲泡还是浸泡,我们都能给它一个新的方法名称,比如说 brew()
。同理,不管是加糖和牛奶,还是加柠檬,我们都可以称之为 addCondiments()
。
现在我们可以创建一个抽象父类来表示泡一杯饮料的整个过程。无论是咖啡(Coffee),还是茶(Tea),都被我们用饮料(Beverage)来表示,最后我们来看一下具体实现。
创建 Beverage 抽象类
abstract class Beverage {
boilWater() {
console.log("把水煮沸");
}
abstract brew(): void;
abstract pourInCup(): void;
abstract addCondiments(): void;
// 模板方法
makeBeverage() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
}
创建 Coffee 类
class Coffee extends Beverage {
brew(): void {
console.log("用沸水冲泡咖啡");
}
pourInCup(): void {
console.log("把咖啡倒进杯子");
}
addCondiments(): void {
console.log("加糖和牛奶");
}
}
创建 Tea 类
class Tea extends Beverage {
brew(): void {
console.log("用沸水浸泡茶叶");
}
pourInCup(): void {
console.log("把茶倒进杯子");
}
addCondiments(): void {
console.log("加柠檬");
}
}
使用示例
function show(): void {
const coffee: Coffee = new Coffee();
const tea: Tea = new Tea();
coffee.makeBeverage();
tea.makeBeverage();
}
以上代码成功运行后会输出以下结果:
把水煮沸
用沸水冲泡咖啡
把咖啡倒进杯子
加糖和牛奶
把水煮沸
用沸水浸泡茶叶
把茶倒进杯子
加柠檬
六、总结
在模板方法设计模式中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。模板方法定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
本人的全栈修仙之路订阅号,会定期分享 Angular、TypeScript、Node.js/Java 、Spring 相关文章,欢迎感兴趣的小伙伴订阅哈!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。