【再谈设计模式】模板方法模式 - 算法骨架的构建者

ops/2025/1/16 18:35:27/

一、引言

        在软件工程、软件开发过程中,我们经常会遇到一些算法或者业务逻辑具有固定的流程步骤,但其中个别步骤的实现可能会因具体情况而有所不同的情况。模板方法设计模式(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 {@Overridevoid chooseClassSpecificOptions() {System.out.println("选择武器类型");}
}class MageCreator extends GameCharacterCreator {@Overridevoid chooseClassSpecificOptions() {System.out.println("选择魔法元素");}
}class AssassinCreator extends GameCharacterCreator {@Overridevoid chooseClassSpecificOptions() {System.out.println("选择隐匿技能类型");}
}

类图:

        - GameCharacterCreator是一个抽象类,有一个公共的createCharacter方法(用+表示),一些私有方法(用-表示)和一个抽象方法(用#表示)。
        - WarriorCreatorMageCreatorAssassinCreator都是继承自GameCharacterCreator的具体类,并且实现了抽象方法chooseClassSpecificOptions

时序图: 

        以WarriorCreator为例,假设玩家创建战士角色。首先GameCharacterCreator引导玩家进行种族和性别的选择,然后WarriorCreator引导玩家进行战士特定的选项选择(这里是武器类型)。对于MageCreatorAssassinCreator可以类似表示,只是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, abstractmethodclass 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("选择性别")@abstractmethoddef choose_class_specific_options(self):passclass 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 mainimport "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,那么在模板方法中就会执行添加调料的步骤,否则就跳过这个步骤。
  • 模板方法与策略模式结合
    • 可以将模板方法中的某些抽象方法的实现委托给策略模式中的具体策略类。例如,在游戏角色创建系统中,选择武器类型这个步骤可以使用策略模式,将不同的武器选择策略封装成不同的策略类,然后在战士角色创建子类中通过组合的方式使用这些策略类来实现选择武器类型的抽象方法。这样可以进一步提高代码的灵活性和可维护性。

http://www.ppmy.cn/ops/150616.html

相关文章

汉图科技XP356DNL高速激光打印一体机综合性能测评

汉图科技XP356DNL高速激光打印一体机效率方面表现出色&#xff0c;支持A4纸型的高速打印&#xff0c;单面打印速度高达35页/分钟&#xff0c;自动双面打印速度可达32面/分钟&#xff0c;这样的速度在日常办公中能够极大地提高打印效率&#xff0c;减少等待时间&#xff0c;满足…

一文了解汽车嵌入式软件开发Franca IDL 知识

本文主要是对 Franca IDL 的作用和设计意图进行解释说明&#xff0c;并且给出其他具有类似功能的 IDL 的对比。用实际的例子来说明核心设计理念&#xff0c;帮助理解设计意图。相比而言&#xff0c;其他 Franca IDL 文章更加注重参考手册的功能&#xff0c;本文试图探求Franca …

R语言的数据库编程

R语言的数据库编程 引言 在当今大数据时代&#xff0c;数据分析已成为推动各行业发展的重要力量。R语言&#xff0c;作为一种专为统计分析和数据挖掘而设计的编程语言&#xff0c;逐渐成为数据科学家和分析师的首选工具。然而&#xff0c;仅仅使用R语言进行数据分析往往无法满…

Codeforces Round 976 (Div. 2) and Divide By Zero 9.0(A-E)

链接&#xff1a;Dashboard - Codeforces Round 976 (Div. 2) and Divide By Zero 9.0 - Codeforces A. Find Minimum Operations 思路 可以观察发现这里有个进制的思想&#xff0c;转换为k进制把每位数相加即可 代码 void solve(){int n,k;cin>>n>>k;if(k1){…

【网络云SRE运维开发】2025第2周-每日【2025/01/12】小测-【第12章 rip路由协议】理论和实操考试题解析

文章目录 选择题答案及解析理论题答案及解析实操题答案及解析下一步进阶 选择题答案及解析 RIP路由协议是基于哪种算法的动态路由协议&#xff1f; 答案&#xff1a;B. 距离矢量算法解析&#xff1a;链路状态算法用于OSPF等协议&#xff1b;最小生成树算法主要用于生成树协议&…

基于Springboot: 宠物小程序开发笔记(上)

概要设计 提供便捷的宠物服务预约平台&#xff0c; 帮助萌宠预约洗护、上门遛狗狗&#xff0c;上门喂猫&#xff0c;驱虫给药等&#xff1b;主要功能包括&#xff1a;展示不同服务&#xff0c;选择日期和时间&#xff0c;完成服务预约&#xff0c;用户查看历史订单和预约状态等…

【C语言】字符串函数详解

文章目录 Ⅰ. strcpy -- 字符串拷贝1、函数介绍2、模拟实现 Ⅱ. strcat -- 字符串追加1、函数介绍2、模拟实现 Ⅲ. strcmp -- 字符串比较1、函数介绍2、模拟实现 Ⅳ. strncpy、strncat、strncmp -- 可限制操作长度Ⅴ. strlen -- 求字符串长度1、函数介绍2、模拟实现&#xff08…

LabVIEW启动时Access Violation 0xC0000005错误

问题描述 在启动LabVIEW时&#xff0c;可能出现程序崩溃并提示以下错误&#xff1a;Error 0xC0000005 (Access Violation) ​ Access Violation错误通常是由于权限不足、文件冲突或驱动问题引起的。以下是解决此问题的全面优化方案&#xff1a; 解决步骤 1. 以管理员身份运行…