c++策略模式
场景设定
我们需要设计一个 Hero(英雄)类,他有一个 Attack(攻击)的行为。
英雄可以使用**“剑”攻击,也可以用“火球”攻击,甚至可以“空手”**攻击。
1. 传统的做法(虚函数 + 继承)
在传统 OOP 中,如果不使用策略模式,你可能会觉得:既然攻击方式不同,那就派生出不同的子类吧。
// 基类 class Hero { public: virtual void Attack() { cout << "空手攻击!" << endl; } // 默认虚函数 }; // 派生类:剑士 class Swordsman : public Hero { public: void Attack() override { cout << "挥剑斩击!" << endl; } }; // 派生类:法师 class Mage : public Hero { public: void Attack() override { cout << "发射火球!" << endl; } }; 这种写法的痛点:
- 死板:一旦你创建了一个
Swordsman对象,他就永远只能挥剑。如果他捡到一根魔杖想变成法师怎么办?你没法把一个Swordsman对象直接变成Mage对象。 - 类爆炸:如果你有 100 种武器,你难道要写 100 个
Hero的子类吗?
2. 新写法(std::function + 策略注入)
现在我们用你提到的方法。Hero 不再有子类,它只是一个“容器”,它的 Attack 行为由一个 std::function 变量决定。
我们可以把这个变量理解为英雄装备的**“技能卡槽”**。
A. 定义 Hero 类
#include <iostream> #include <functional> class Hero; // 前置声明 // 定义接口类型:这是一个“函数签名”,它接受一个 Hero 引用,没有返回值 using AttackStrategy = std::function<void(const Hero&)>; // 默认的攻击方式(普通函数) void DefaultPunch(const Hero& h) { std::cout << "英雄使用了:普通拳击 (默认)" << std::endl; } class Hero { public: // 构造函数:默认装备“普通拳击”技能卡 Hero(AttackStrategy strategy = DefaultPunch) : attack_skill(strategy) {} // 公开的非虚函数接口 void DoAttack() { std::cout << "[准备动作] 英雄深吸一口气..." << std::endl; // 核心:调用当前装备的技能卡 attack_skill(*this); std::cout << "[结束动作] 攻击完成。" << std::endl; } // 关键点:我们可以随时更换技能卡! void ChangeWeapon(AttackStrategy new_strategy) { attack_skill = new_strategy; } private: AttackStrategy attack_skill; // 这里保存了具体的“怎么打” }; B. 三种不同的实现(举例说明)
现在,可以通过给 attack_skill 赋值不同的内容,让同一个 Hero 对象表现出完全不同的样子。
例子 1:普通函数(最简单的实现)
// 一个普通的全局函数 void UseSword(const Hero& h) { std::cout << ">>> EXCALIBUR! (使用圣剑攻击)" << std::endl; } // 使用: Hero h1(UseSword); h1.DoAttack(); // 输出: // [准备动作] ... // >>> EXCALIBUR! ... 例子 2:Lambda 表达式(最方便的实现)
有时候我们不想专门写个函数,只想临时定义一个招式,比如“扔石头”。
// 使用 Lambda 直接定义逻辑 Hero h2([](const Hero& h) { std::cout << ">>> 扔出了一块石头!(Lambda实现)" << std::endl; }); h2.DoAttack(); 例子 3:成员函数 + std::bind(最复杂的实现,也是你困惑的地方)
假设我们有一个复杂的 MagicBook(魔法书)类,里面存了很多法术。这些法术是 MagicBook 的成员函数,而不是全局函数。
class MagicBook { public: void CastFireball(const Hero& h) { std::cout << ">>> 这里的魔法书:发射大火球!" << std::endl; } void CastIceStorm(const Hero& h) { std::cout << ">>> 这里的魔法书:召唤暴风雪!" << std::endl; } }; 现在问题来了:MagicBook::CastFireball 也是一个函数,但它的参数其实有两个:(MagicBook* this, const Hero& h)。
而我们的 std::function 只接受一个参数 (const Hero& h)。签名不匹配!
这就好比插头(函数)是三孔的,但插座(std::function)是两孔的。
std::bind 就是那个转接头。 它把 MagicBook 的对象实例(this)直接“焊死”在函数上,让它对外看起来只剩下一个参数。
int main() { MagicBook myBook; // 我有一本魔法书对象 // 创建一个英雄 Hero h3; // 1. 英雄想用魔法书里的火球术 // 这里的 bind 意思:把 myBook 绑定到 CastFireball 的第一个参数(this)上 // std::placeholders::_1 表示:Hero 参数留空,等调用的时候再填 h3.ChangeWeapon(std::bind(&MagicBook::CastFireball, &myBook, std::placeholders::_1)); h3.DoAttack(); // 输出:发射大火球! // 2. 英雄想换成冰风暴 h3.ChangeWeapon(std::bind(&MagicBook::CastIceStorm, &myBook, std::placeholders::_1)); h3.DoAttack(); // 输出:召唤暴风雪! return 0; } 总结对比
| 特性 | 传统虚函数写法 | std::function 写法 (策略模式) |
| 实现方式 | class Swordsman : public Hero | h.ChangeWeapon(UseSword) |
| 切换行为 | 不可能 (你是剑士就一辈子是剑士) | 极易 (运行 |