c/c++设计模式--模板方法模式
模板方法模式是一种行为设计模式,它定义了一个算法的骨架,并允许子类在不改变该算法结构的情况下重写算法的特定步骤。这种模式属于行为型模式,它通过将具体实现延迟到子类来提供算法的变化点。
在模板方法模式中,通常有两种角色:
-
抽象类(Abstract Class):定义了一个算法的骨架,其中包含了一系列抽象方法或者受保护的方法,这些方法是算法的各个步骤。抽象类可以包含一个模板方法,该方法定义了算法的结构,通常是一个具体方法,它调用了算法中的各个步骤。这些步骤可以是抽象方法,由子类去实现,也可以是具体方法,提供了默认实现。
-
具体子类(Concrete Subclasses):实现了抽象类中定义的抽象方法,以提供算法的具体实现。这些具体子类通常只需要实现抽象方法中与特定实现相关的部分,而不需要改变算法的整体结构。
模板方法模式的核心思想是将算法的不变部分封装在父类中,将可变部分留给子类去实现,从而实现了代码的重用和扩展。这种模式常用于以下场景:
- 当多个类中有相似的算法,并且这些算法中的部分步骤是固定不变的,而其他部分需要根据具体情况进行实现时,可以使用模板方法模式。
- 当需要提供一个通用的算法框架,而具体的步骤实现可能会随着不同情况而变化时,可以使用模板方法模式。
举个例子,假设有一个制作饮料的模板方法,其中包括了泡茶和泡咖啡两个步骤,但是具体的泡茶和泡咖啡的步骤可能有所不同,可以使用模板方法模式来实现这个过程
下面是一个简单的 C++ 示例,演示了模板方法模式的实现。假设我们有一个制作饮料的过程,包括煮水、冲泡、倒入杯子和添加调料。我们可以使用模板方法模式来定义这个过程的框架,并在子类中实现具体的步骤。
#include <iostream> // 抽象类 - 制作饮料 class Beverage { public: // 模板方法 - 制作饮料的整个过程 void prepareBeverage() { boilWater(); brew(); pourInCup(); addCondiments(); } // 具体步骤 - 煮水 void boilWater() { std::cout << "Boiling water" << std::endl; } // 抽象步骤 - 冲泡 virtual void brew() = 0; // 具体步骤 - 倒入杯子 void pourInCup() { std::cout << "Pouring into cup" << std::endl; } // 抽象步骤 - 添加调料 virtual void addCondiments() = 0; }; // 具体子类 - 制作茶 class Tea : public Beverage { public: // 实现具体步骤 - 冲泡茶 void brew() override { std::cout << "Steeping the tea" << std::endl; } // 实现具体步骤 - 添加柠檬 void addCondiments() override { std::cout << "Adding lemon" << std::endl; } }; // 具体子类 - 制作咖啡 class Coffee : public Beverage { public: // 实现具体步骤 - 冲泡咖啡 void brew() override { std::cout << "Dripping coffee through filter" << std::endl; } // 实现具体步骤 - 添加糖和牛奶 void addCondiments() override { std::cout << "Adding sugar and milk" << std::endl; } }; int main() { Beverage* tea = new Tea(); Beverage* coffee = new Coffee(); std::cout << "Making tea..." << std::endl; tea->prepareBeverage(); std::cout << "\nMaking coffee..." << std::endl; coffee->prepareBeverage(); delete tea; delete coffee; return 0; }
模板方法(Template Method)模式
//饭馆吃饭: 点餐(粤菜,鲁菜)->食用->结账(现金,信用卡,微信),因为这几个步骤是固定的所以作为以样板。
//在固定步骤确定的情况下,通过多态机制在多个子类中对每个步骤的细节进行差异化实现,这就是模板方法模式能够达到的效果。
//模板方法模式:行为型模式。
//(1)一个具体实现范例的逐步重构
//A公司有一个小游戏项目组——开发单机闯关打斗类游戏(类似街机打拳类游戏)。
//一个游戏项目组最少需要三名担任不同角色的员工组成:游戏策划,游戏程序,游戏美术。
//a)游戏策划:简称策划 ,负责提出游戏的各种玩法需求,确定游戏中各种数值比如人物(敌人)的生命值、魔法值。
//b)游戏程序:简称程序,需要与游戏策划紧密配合通过代码来实现游戏策划要求的各种游戏功能。
//c)游戏美术:角色设计,道具设计,游戏特效等等。
//游戏策划需求:游戏主角是个战士(攻击力不够强,生命值比较多,抗揍),主角通过不断的往前走来闯关,遇到敌人就进行攻击,
//敌人也会反击,敌人也会距离近时主动攻击主角。
//主角:生命值——1000(为0时主角死亡,游戏结束),魔法值——0(暂时用不上,保留),攻击力——200(打敌人一下敌人失去多少点声明值) 三个属性。
//技能“燃烧”——使用该技能可以使附近所有敌人每人失去500点生命值,但是主角自身也会损失掉300点生命值。
//增加 法师 作为主角(攻击力很强,生命值比较少,不抗揍)
//主角:生命值——800,魔法值——200(暂时用不上,保留),攻击力——300 三个属性。
//技能“燃烧”——使用该技能可以使附近所有敌人每人失去650点生命值,但是主角自身会损失掉100点魔法值。
//将要增加 牧师 作为主角
//每个主角都有一个叫做 燃烧 的技能。每个主角释放 燃烧技能时效果各不相同。有两点是肯定不变的:对主角自身会产生影响,对敌人会产生影响。
//战士和法师释放 燃烧 技能表现是不同的,这种不同的表现主要是通过F_Warrior和F_Mage子类中的effect_enemy和effect_self虚函数来体现的。
namespace _nmsp2 { //战斗者父类 class Fighter { public: Fighter(int life, int magic, int attack) :m_life(life), m_magic(magic), m_attack(attack) {} virtual ~Fighter() {} //做父类时析构函数应该为虚函数 //对主角自身会产生影响,对敌人会产生影响。 //分析:对敌人产生影响,有函数effect_enemy。对主角自身产生影响,有函数effect_self。播放技能play_effect函数。 void JN_Burn() //技能“燃烧”,模板方法 { if (canUseJN() == false) //如果不能使用该技能,则直接返回 return; effect_enemy(); //对敌人产生的影响 effect_self(); //对主角自身产生的影响 play_effect(); //播放技能“燃烧”的技能特效 } private: virtual void effect_enemy() {} //函数体为空,表示啥也不做,如果要求必须在子类中重新实现该虚函数,则可以将该函数写成纯虚函数。 virtual void effect_self() {} void play_effect() { cout << "播放技能\"燃烧\"的技能特效给玩家看" << endl; //所有主角播放的技能特效都相同,因此不用写成一个虚函数并在子类中实现技能特效的播放。 } virtual bool canUseJN() = 0; //判断是否能使用技能“燃烧”,这是个纯虚函数声明,子类中必须重新实现canUseJN。 protected: //可能被子类访问,所以用protected修饰 //角色属性 int m_life; //生命值 int m_magic; //魔法值 int m_attack; //攻击力 }; //------------------------- //“战士”类,父类为Fighter class F_Warrior :public Fighter { public: F_Warrior(int life, int magic, int attack) :Fighter(life,magic,attack) {} private: //对敌人产生的影响 virtual void effect_enemy() { cout << "战士主角_让所有敌人每人失去500点生命,相关逻辑代码这里略......" << endl; } //对主角自身产生的影响 virtual void effect_self() { cout << "战士主角_自身失去300点生命值" << endl; m_life -= 300; } virtual bool canUseJN() { if (m_life < 300) //生命值不够300点,不能使用技能“燃烧” return false; return true; } }; //------------------------- //“法师”类,父类为Fighter class F_Mage :public Fighter { public: F_Mage(int life, int magic, int attack) :Fighter(life, magic, attack) {} private: //对敌人产生的影响 virtual void effect_enemy() { cout << "法师主角_让所有敌人每人失去650点生命,相关逻辑代码这里略......" << endl; } //对主角自身产生的影响 virtual void effect_self() { cout << "法师主角_自身失去100点魔法值" << endl; m_magic -= 100; } virtual bool canUseJN() { if (m_magic < 100) //魔法值不够100点,不能使用技能“燃烧” return false; return true; } }; } int main() { /* _nmsp2::Fighter* prole_war = new _nmsp2::F_Warrior(1000, 0, 200); //创建战士主角,注意这里是父类指针指向子类对象以利用多态特性。 prole_war->JN_Burn(); //战士主角释放“燃烧”技能,调用的是F_Warrior类的effect_enemy和effect_self。 cout << "---------------------------" << endl; //分割线,以便更醒目的显示信息 _nmsp2::Fighter* prole_mag = new _nmsp2::F_Mage(800,200, 300); //创建法师主角,注意这里是父类指针指向子类对象以利用多态特性。 prole_mag->JN_Burn(); //法师主角释放“燃烧”技能 //释放资源 delete prole_war; delete prole_mag; */ ////早绑定 //_nmsp2::F_Warrior role_war(1000, 0, 200); //role_war.JN_Burn(); //早绑定 _nmsp2::Fighter* prole_war2 = new _nmsp2::F_Warrior(50, 0, 200);//创建生命值只有50的战士主角 prole_war2->JN_Burn(); //该战士无法成功释放“燃烧”技能,不输出任何结果。 //钩子方法 delete prole_war2; }
引入模板方法(Template Method)模式
//软件开发中需求变化频繁的,开发人员要尝试寻找变化点,把变化部分和稳定部分分离开来,在变化的地方应用设计模式。
//学习设计模式并不难,难的是在何时何地运用该模式。
//设计模式中往往会把 成员函数 说成是 算法。
//晚绑定:代码执行时才知道具体要执行哪个虚函数。
//早绑定:编译时就能确定执行的是哪个子类(F_Warrior或者F_Mage中的effect_enemy或者effect_self)。
//模板方法模式的定义(实现意图):定义了一个操作中的算法的骨架(稳定部分),而将一些步骤延迟到子类中去实现(父类中定义虚函数,子类中实现/重写这个虚函数),
//从而达到在整体稳定的情况下能够产生一些变化的目的。
//设计模式的经典总结:设计模式的作用就是在变化和稳定中间寻找隔离点,分离稳定和变化,从而来管理变化。
//模板方法模式也被认为导致了一种反向控制结构——这种结构被称为好莱坞法则——不要来调用我,我会去调用你。
//(3)模板方法(Template Method)模式的UML图
//UML:Unified Modeling Language:统一建模语言。
//UML:一种工具,通过该工具可以绘制一个类的结构图和类与类之间的关系。这种把所编写的代码以图形方式呈现对于代码的全局理解和掌握好处巨大。
//(4)程序代码的进一步完善及应用联想
//钩子方法:子类勾住父类从而反向控制父类行为的意思,因此起名为钩子方法。
//MFC框架(微软基础类库):通过MFC创建一个基于对话框的应用程序。自动调用OnInitDialog成员函数(相当于effect_enemy或者effect_self这样的虚函数)。
//车间能够装配很多零件。如果零件的装配工序非常固定,工序细节有微小变化,就可以
//针对零件创建一个父类,其中零件装配工序(成员函数)就非常适合采用模板方法模式来实现,而处理某道工序的细节可以直接放在子类(针对某个具体零件的类)虚函数中进行。