【再谈设计模式】模板方法模式 - 算法骨架的构建者
一、引言
在软件工程、软件开发过程中,我们经常会遇到一些算法或者业务逻辑具有固定的流程步骤,但其中个别步骤的实现可能会因具体情况而有所不同的情况。模板方法设计模式(Template Method Design Pattern)就为解决这类问题提供了一个优雅的方案。它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中去实现,使得子类可以在不改变算法结构的情况下重新定义某些特定的步骤。
二、定义与描述
模板方法设计模式是一种行为型设计模式。它包含一个抽象类(在Java和C++中)或者一个抽象基类(在Python中可以通过ABC抽象基类实现类似功能,在Go中通过接口和结构体组合来体现),这个抽象类中定义了一个模板方法,这个模板方法包含了算法的骨架,它按照一定的顺序调用其他的抽象方法或具体方法。抽象方法由子类去实现,从而实现不同的行为。
三、抽象背景
假设我们正在开发一个游戏角色创建系统。游戏中有不同类型的角色,如战士、法师、刺客。每个角色在创建时有一些通用的步骤,例如选择种族、选择性别等,但也有一些特定于角色类型的步骤,比如战士要选择武器类型,法师要选择魔法元素,刺客要选择隐匿技能类型。这时候就可以使用模板方法设计模式来构建这个创建系统。
四、适用场景与现实问题解决
- 场景一:框架开发
- 在框架开发中,框架通常提供了一个固定的处理流程,但允许用户自定义某些特定的操作。例如,Web框架可能定义了处理HTTP请求的基本流程:接收请求、解析请求、处理业务逻辑、构建响应、发送响应。其中处理业务逻辑的部分可以由用户根据自己的需求定制。
- 使用模板方法设计模式,框架可以将整个请求处理流程定义在一个抽象类中的模板方法里,而将处理业务逻辑的部分抽象成抽象方法,让用户通过继承抽象类并实现抽象方法来定制自己的业务逻辑。
- 场景二:算法流程固定但部分可变
- 例如排序算法中的希尔排序。希尔排序的基本思想是将数组按照一定的间隔进行分组,然后对每组进行插入排序,逐渐缩小间隔直到间隔为1。其中分组的计算和整体的排序流程是固定的,但每次分组后的插入排序步骤(比较和交换元素的操作)可以看作是一个可变的部分。
- 可以使用模板方法设计模式,将希尔排序的整体流程定义在模板方法中,而将插入排序的操作抽象成抽象方法,这样如果要对插入排序进行优化或者修改,只需要在子类中重新实现这个抽象方法即可。
五、模板方法设计模式的现实生活的例子
- 泡茶示例
- 泡茶的基本步骤是固定的:准备茶具、烧开水、浸泡茶叶、倒入茶杯、添加调料(如糖、柠檬等,这一步可选)。这里烧开水、浸泡茶叶等步骤是固定的顺序,但不同的茶叶(如绿茶、红茶、黑茶)浸泡的时间和温度可能不同,添加调料的种类也可能不同。
- 可以将泡茶的过程看作一个模板方法,其中准备茶具、烧开水等是固定的步骤,而浸泡茶叶和添加调料可以看作是抽象方法,根据不同的茶叶种类(子类)来具体实现。
六、初衷与问题解决
初衷是为了在具有固定流程的算法或者业务逻辑中,提高代码的复用性和可维护性。通过将固定的流程放在模板方法中,将可变的部分抽象出来由子类实现,可以避免代码的重复编写,并且当业务逻辑发生变化时,只需要修改对应的子类即可,而不需要修改整个算法的结构。
七、代码示例
Java示例
abstract class GameCharacterCreator {
// 模板方法,定义了创建角色的流程
public final void createCharacter() {
chooseRace();
chooseGender();
chooseClassSpecificOptions();
}
private void chooseRace() {
System.out.println("选择种族");
}
private void chooseGender() {
System.out.println("选择性别");
}
// 抽象方法,由子类实现
abstract void chooseClassSpecificOptions();
}
class WarriorCreator extends GameCharacterCreator {
@Override
void chooseClassSpecificOptions() {
System.out.println("选择武器类型");
}
}
class MageCreator extends GameCharacterCreator {
@Override
void chooseClassSpecificOptions() {
System.out.println("选择魔法元素");
}
}
class AssassinCreator extends GameCharacterCreator {
@Override
void chooseClassSpecificOptions() {
System.out.println("选择隐匿技能类型");
}
}
类图:
- GameCharacterCreator
是一个抽象类,有一个公共的createCharacter
方法(用+
表示),一些私有方法(用-
表示)和一个抽象方法(用#
表示)。
- WarriorCreator
、MageCreator
和AssassinCreator
都是继承自GameCharacterCreator
的具体类,并且实现了抽象方法chooseClassSpecificOptions
。
时序图:
以WarriorCreator
为例,假设玩家创建战士角色。首先GameCharacterCreator
引导玩家进行种族和性别的选择,然后WarriorCreator
引导玩家进行战士特定的选项选择(这里是武器类型)。对于MageCreator
和AssassinCreator
可以类似表示,只是chooseClassSpecificOptions
的内容不同。
流程图:
首先进行种族和性别的选择,然后根据选择的角色类型(战士、法师或刺客等)执行相应子类的特定选项选择方法,如果是未知角色类型则给出提示。
C++示例
class GameCharacterCreator {
public:
// 模板方法,定义了创建角色的流程,final表示不能被子类重写
void createCharacter() {
chooseRace();
chooseGender();
chooseClassSpecificOptions();
}
private:
void chooseRace() {
std::cout << "选择种族" << std::endl;
}
void chooseGender() {
std::cout << "选择性别" << std::endl;
}
// 纯虚函数,相当于抽象方法,由子类实现
virtual void chooseClassSpecificOptions() = 0;
};
class WarriorCreator : public GameCharacterCreator {
public:
void chooseClassSpecificOptions() override {
std::cout << "选择武器类型" << std::endl;
}
};
class MageCreator : public GameCharacterCreator {
public:
void chooseClassSpecificOptions() override {
std::cout << "选择魔法元素" << std::endl;
}
};
class AssassinCreator : public GameCharacterCreator {
public:
void chooseClassSpecificOptions() override {
std::cout << "选择隐匿技能类型" << std::endl;
}
};
Python示例
from abc import ABC, abstractmethod
class GameCharacterCreator(ABC):
def create_character(self):
self.choose_race()
self.choose_gender()
self.choose_class_specific_options()
def choose_race(self):
print("选择种族")
def choose_gender(self):
print("选择性别")
@abstractmethod
def choose_class_specific_options(self):
pass
class WarriorCreator(GameCharacterCreator):
def choose_class_specific_options(self):
print("选择武器类型")
class MageCreator(GameCharacterCreator):
def choose_class_specific_options(self):
print("选择魔法元素")
class AssassinCreator(GameCharacterCreator):
def choose_class_specific_options(self):
print("选择隐匿技能类型")
Go示例
package main
import "fmt"
// 抽象结构体
type GameCharacterCreator struct{}
// 模板方法
func (g *GameCharacterCreator) createCharacter() {
g.chooseRace()
g.chooseGender()
g.chooseClassSpecificOptions()
}
func (g *GameCharacterCreator) chooseRace() {
fmt.Println("选择种族")
}
func (g *GameCharacterCreator) chooseGender() {
fmt.Println("选择性别")
}
// 抽象方法,由具体结构体实现
type CharacterCreator interface {
chooseClassSpecificOptions()
}
type WarriorCreator struct{}
func (w *WarriorCreator) chooseClassSpecificOptions() {
fmt.Println("选择武器类型")
}
type MageCreator struct{}
func (m *MageCreator) chooseClassSpecificOptions() {
fmt.Println("选择魔法元素")
}
type AssassinCreator struct{}
func (a *AssassinCreator) chooseClassSpecificOptions() {
fmt.Println("选择隐匿技能类型")
}
八、模板方法设计模式的优缺点
优点
- 提高代码复用性
- 算法的骨架在抽象类中定义一次,多个子类可以复用这个模板方法,减少了代码的重复编写。
- 可维护性增强
- 当业务逻辑发生变化时,只需要修改抽象类中的模板方法或者子类中的具体实现,而不需要对整个系统进行大规模的修改。
- 便于代码的扩展
- 可以很容易地添加新的子类来实现不同的具体行为,只要遵循抽象类中定义的模板方法结构。
缺点
- 违反开闭原则
- 如果要对模板方法中的算法骨架进行修改,可能需要修改抽象类,这就违反了开闭原则(对扩展开放,对修改关闭)。
- 类层次结构复杂
- 随着子类的增加,类的层次结构可能会变得比较复杂,导致代码的理解和维护成本增加。
九、模板方法设计模式的升级版
- 钩子方法(Hook Method)
- 钩子方法是一种在模板方法模式中常用的扩展机制。它是在抽象类中定义的一个空的或者有默认实现的方法,子类可以选择性地重写这个方法。例如,在泡茶的例子中,可以添加一个钩子方法“isAddSeasoning”,如果子类(某种茶叶)重写这个方法并返回true,那么在模板方法中就会执行添加调料的步骤,否则就跳过这个步骤。
- 模板方法与策略模式结合
- 可以将模板方法中的某些抽象方法的实现委托给策略模式中的具体策略类。例如,在游戏角色创建系统中,选择武器类型这个步骤可以使用策略模式,将不同的武器选择策略封装成不同的策略类,然后在战士角色创建子类中通过组合的方式使用这些策略类来实现选择武器类型的抽象方法。这样可以进一步提高代码的灵活性和可维护性。