第四章 策略模式

第四章 策略模式

第一节 一个具体实现范例的逐步重构

1.1课程引入与需求背景

1.1.1课程衔接

  上一章完成工厂模式、原型模式、建造者模式的学习,本章进入行为型设计模式—— 策略模式。策略模式的核心思想与模板方法模式类似,均以 “扩展方式支持未来变化”,本章通过具体范例重构引入模式,下一节将讲解依赖倒置原则。

1.1.2业务需求

  以单机闯关打斗游戏为场景,需为游戏主角(战士、法师)添加补血道具功能

  • 道具类型(药品):补血丹(+200 生命值)、大还丹(+300 生命值)、守护丹(+500 生命值)
  • 触发条件:主角走到特定场景或击杀大型怪物后,触碰道具即可补血
  • 技术基础:已存在Fighter(战斗者父类)、F_Warrior(战士子类)、F_Mage(法师子类)

1.2原始实现方案及问题分析

1.2.1原始代码实现

(1)Fighter.h 核心部分
#ifndef __RIGHTER__
#define __RIGHTER__

// 定义道具枚举
enum ItemAddlife 
{
    LF_BXD,  // 补血丹
    LF_DHD,  // 大还丹
    LF_SHD   // 守护丹
};

//战斗者父类
class Fighter 
{
public:
    Fighter(int life, int magic, int attack) :m_life(life), m_magic(magic), m_attack(attack) {}
	virtual ~Fighter() {}
    
public:
    // 吃药补血的核心函数
    void UseItem(ItemAddlife djtype) 
    {
        if (djtype == LF_BXD)
        {
            m_life += 200; // 补血丹+200
        } else if (djtype == LF_DHD) 
        {
            m_life += 300; // 大还丹+300
        } else if (djtype == LF_SHD) 
        {
            m_life += 500; // 守护丹+500
        }
        // 未来扩展功能需添加更多if-else
    }
    // 其他成员(构造、析构、成员变量等)
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) 
    {
        cout << "战士角色创建成功!" << endl;
    }

    ~F_Warrior() 
    {
        cout << "战士角色析构" << endl;
    }
};

// 法师子类(继承Fighter)
class F_Mage : public Fighter 
{
public:
    // 构造函数:调用父类构造初始化
    F_Mage(int life, int magic, int attack) : Fighter(life, magic, attack) 
    {
        cout << "法师角色创建成功!" << endl;
    }

    ~F_Mage() 
    {
        cout << "法师角色析构" << endl;
    }
};
#endif
(2)MyProject.cpp
// MyProject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
//公众号:程序员速成 ,内含一辈子都让你感激自己的优质视频教程,欢迎关注

#include <iostream>
#include "Fighter.h"
#include "ItemStrategy.h"

#ifdef _DEBUG   //只在Debug(调试)模式下
#ifndef DEBUG_NEW
#define DEBUG_NEW new(_NORMAL_BLOCK,__FILE__,__LINE__) //重新定义new运算符
#define new DEBUG_NEW
#endif
#endif

//#include <boost/type_index.hpp>
using namespace std;
//#pragma warning(disable : 4996) 

namespace _nmsp1
{	
	
}

int main() {
    // 初版直接创建Fighter对象(无战士/法师子类)
    Fighter* prole_war = new Fighter(1000, 0, 200);  // 主角:1000血、0蓝、200攻击
    
    // 测试使用三种道具(按初版需求顺序测试)
    cout << "\n=== 开始使用道具 ===" << endl;
    prole_war->UseItem(LF_DHD);  // 先使用大还丹
    
    // 释放资源(避免内存泄漏)
    delete prole_war;

    return 0;
}

1.2.2原始方案的三大核心问题

  1. 违反开闭原则:新增道具时,需同时修改enum枚举和UseItem中的if-else分支,直接修改原有代码
  2. 代码复用性差:若怪物也需使用补血道具,需复制UseItem的所有判断逻辑到怪物类,导致代码冗余
  3. 逻辑扩展性差:若道具功能升级(如补血 + 解毒、狂暴状态下额外补血 / 补蓝),UseItem会堆积大量逻辑,函数臃肿难以维护

1.3策略模式重构方案(核心重点)

1.3.1策略模式核心思想

UseItem中每个if-else分支(即 “每种道具的补血算法”)封装为独立的策略类,通过抽象策略类统一接口,让算法可相互替换,最终解决原始方案的问题。

1.3.2重构步骤与代码实现

步骤 1:创建抽象策略类(ItemStrategy.h)

定义所有策略(道具)的公共接口,声明UseItem纯虚函数(参数为Fighter*,用于访问主角的生命值数据):

#ifndef _ITEMSTRATEGY__
#define _ITEMSTRATEGY__

// 前向声明Fighter类(因策略类需访问Fighter的接口)
class Fighter;

// 抽象策略类:所有补血道具的父类
class ItemStrategy 
{
public:
    // 纯虚函数:定义补血算法的接口
    virtual void UseItem(Fighter* mainobj) = 0;
    virtual ~ItemStrategy() {} // 虚析构函数,确保子类析构正常
};

#endif
步骤 2:实现具体策略类(ItemStrategy.h 续)

每个具体策略类对应一种道具,实现抽象策略类的UseItem接口,封装专属补血逻辑:

#ifndef _ITEMSTRATEGY__
#define _ITEMSTRATEGY__

//道具策略类的父类
class ItemStrategy
{
public:
	virtual void UseItem(Fighter* mainobj) = 0;
	virtual ~ItemStrategy() {}
};

//补血丹策略类
class ItemStrategy_BXD :public ItemStrategy
{
public:
	virtual void UseItem(Fighter* mainobj)
	{
		mainobj->SetLife(mainobj->GetLife() + 200);  //补充200点生命值
	}
};

//大还丹策略类
class ItemStrategy_DHD :public ItemStrategy
{
public:
	virtual void UseItem(Fighter* mainobj)
	{
		mainobj->SetLife(mainobj->GetLife() + 300);  //补充300点生命值
	}
};

//守护丹策略类
class ItemStrategy_SHD :public ItemStrategy
{
public:
	virtual void UseItem(Fighter* mainobj)
	{
		mainobj->SetLife(mainobj->GetLife() + 500);  //补充500点生命值
	}
};

#endif
步骤 3:改造环境类(Fighter.h)

Fighter作为环境类,不再直接包含补血逻辑,而是通过持有抽象策略类的指针,动态切换策略(道具):

#ifndef __RIGHTER__
#define __RIGHTER__

class ItemStrategy; // 前向声明策略类

class Fighter 
{
public:
    Fighter(int life, int magic, int attack) 
        : m_life(life), m_magic(magic), m_attack(attack) {}
    virtual ~Fighter() {}

    // 核心接口:设置当前使用的道具策略
    void SetItemStrategy(ItemStrategy* strategy) 
    {
        itemstrategy = strategy;
    }

    // 核心接口:执行补血(调用当前策略的算法)
    void UseItem() 
    {
        if (itemstrategy != nullptr) 
        {
            // 传入this指针,让策略类访问当前对象的生命值
            itemstrategy->UseItem(this);
        }
    }

    // 提供生命值的读写接口(供策略类调用)
    int GetLife() { return m_life; }
    void SetLife(int life) { m_life = life; }

protected:
    int m_life;     // 生命值
    int m_magic;    // 魔法值
    int m_attack;   // 攻击力
    // 持有抽象策略类的指针(C++11支持直接初始化 nullptr)
    ItemStrategy* itemstrategy = nullptr;
};

// 战士子类(无修改,继承Fighter的所有接口)
class F_Warrior : public Fighter 
{
public:
    F_Warrior(int life, int magic, int attack) : Fighter(life, magic, attack) {}
};

// 法师子类(无修改)
class F_Mage : public Fighter
{
public:
    F_Mage(int life, int magic, int attack) : Fighter(life, magic, attack) {}
};

#endif
步骤 4:实现环境类成员函数(Fighter.cpp)
#include <iostream>
#include "Fighter.h"
#include "ItemStrategy.h"

using namespace std;

// 实现设置策略的接口
void Fighter::SetItemStrategy(ItemStrategy* strategy) 
{
    itemstrategy = strategy;
}

// 实现使用道具的接口
void Fighter::UseItem() 
{
    if (itemstrategy != nullptr) 
    {
        itemstrategy->UseItem(this);
    }
}

// 实现生命值读写接口
int Fighter::GetLife() 
{
    return m_life;
}

void Fighter::SetLife(int life) 
{
    m_life = life;
}
步骤 5:主函数调用(MyProject.cpp)

客户端通过 “创建策略对象→设置策略→执行策略” 的流程使用道具,支持动态切换:

#include <iostream>
#include "Fighter.h"
#include "ItemStrategy.h"

int main() 
{
    // 1. 创建主角(战士,初始生命值1000)
    Fighter* prole_war = new F_Warrior(1000, 0, 200);

    // 2. 吃大还丹(+300)
    ItemStrategy* dhd_strategy = new ItemStrategy_DHD(); // 创建大还丹策略
    prole_war->SetItemStrategy(dhd_strategy); // 设置当前策略为大还丹
    prole_war->UseItem(); // 执行补血(生命值变为1300)

    // 3. 吃补血丹(+200)
    ItemStrategy* bxd_strategy = new ItemStrategy_BXD(); // 创建补血丹策略
    prole_war->SetItemStrategy(bxd_strategy); // 切换策略为补血丹
    prole_war->UseItem(); // 执行补血(生命值变为1500)

    // 4. 释放资源
    delete dhd_strategy;
    delete bxd_strategy;
    delete prole_war;

    return 0;
}

1.3.3关键代码说明

  • 策略类与环境类的耦合:策略类UseItem参数为Fighter*,是因为策略需要访问主角的生命值数据(GetLife/SetLife),这种耦合是合理且必要的
  • 前向声明Fighter.h中声明ItemStrategyItemStrategy.h中声明Fighter,解决跨文件相互引用问题
  • 动态切换策略:通过SetItemStrategy可随时更换道具,无需修改Fighter类代码,符合开闭原则

1.3.4策略模式的核心定义与 UML 类图

(1)模式定义

定义一系列算法(如三种补血逻辑),将每个算法封装到独立的具体策略类中,作为抽象策略类的子类,使得算法可相互替换,客户端可根据需求动态选择使用哪种算法。

(2)UML 类图

(3)三大核心角色
角色名称 职责描述 本范例对应类
Context(环境类) 维持抽象策略类的指针 / 引用,提供接口供客户端设置策略、执行算法 Fighter
Strategy(抽象策略类) 定义所有具体策略的公共接口(纯虚函数),是所有策略类的父类 ItemStrategy
ConcreteStrategy(具体策略类) 继承抽象策略类,实现具体的算法逻辑(如每种道具的补血规则) ItemStrategy_BXD等 3 个类

1.3.5策略模式的优缺点

(1)优点
  1. 符合开闭原则:新增道具时,只需新增具体策略类(如ItemStrategy_XYD),无需修改Fighter或原有策略类,以扩展支持变化
  2. 消除冗余分支:替代大量不稳定的if-else/switch语句(策略模式是 “分支杀手”),代码结构更清晰
  3. 算法复用性高:若怪物类需使用补血道具,只需让怪物类提供GetLife/SetLife接口,即可直接复用现有策略类
  4. 替代继承方案:无需通过继承Fighter生成 “战士 + 补血丹”“战士 + 大还丹” 等子类,通过切换策略即可改变对象行为
(2)缺点
  1. 策略类数量增多:每种算法对应一个类,若道具数量极多,会导致类的数量膨胀
  2. 客户端需了解策略:调用者(如main函数)需熟知所有策略类的功能,才能选择合适的策略(如知道ItemStrategy_DHD是大还丹)

1.3.6适用场景总结

  1. 当代码中存在大量不稳定的if-else/switch分支(如频繁新增的功能选项、道具、规则)
  2. 当需要动态切换算法(如主角战斗时切换不同攻击策略、道具效果)
  3. 当算法需要被多个不同对象复用(如主角和怪物共用补血道具逻辑)
注意事项
  • 若分支逻辑稳定不变(如一周七天、四季循环),无需使用策略模式,直接用if-else更简洁
  • 策略类与环境类的耦合需适度:若策略需要环境类的大量数据,可考虑封装数据传输对象(DTO),减少直接依赖

第二节 依赖倒置原则

1.1依赖倒置原则基础信息

1.1.1基本标识与核心地位

  • 英文名称:Dependency Inversion Principle,简称DIP
  • 核心定位:是面向对象设计的主要实现方法,同时也是实现开闭原则(对扩展开放、对修改关闭)的重要途径,可有效降低高层组件与低层组件间的耦合度,且贯穿于绝大部分设计模式。

1.1.2核心定义

高层组件不应该依赖于低层组件(具体实现类),两者都应该依赖于抽象层。

  • 术语解析
    • 高层组件:指业务逻辑代码(如main函数中主角击杀怪物的调用代码),负责实现核心业务流程。
    • 低层组件:指具体功能实现类(如M_UndeadM_Element等具体怪物类),提供基础功能。
    • 抽象层:指抽象父类或接口(如Monster类),作为高层和低层组件的依赖中介,定义统一接口。

1.2代码案例对比分析(怪物击杀场景)

为体现 DIP 的作用,通过两种实现方案对比说明,对应代码中_nmsp1(违背 DIP)和_nmsp2(遵循 DIP)两个命名空间的实现。

1. 2.1传统写法(_nmsp1命名空间,违背 DIP)

(1)代码实现
namespace _nmsp1
{	
	class M_Undead //亡灵类怪物
	{
	public:
		void getinfo()
		{
			cout << "这是一只亡灵类怪物" << endl;
		}
		//......其他代码略
	};

	class M_Element //元素类怪物
	{
	public:
		void getinfo()
		{
			cout << "这是一只元素类怪物" << endl;
		}
		//......其他代码略
	};

	class M_Mechanic //机械类怪物
	{
	public:
		void getinfo()
		{
			cout << "这是一只机械类怪物" << endl;
		}
		//......其他代码略
	};

	//战士主角
	class F_Warrior
	{
	public:
		void attack_enemy_undead(M_Undead* pobj) //攻击亡灵类怪物
		{
			//进行攻击处理......
			pobj->getinfo(); //可以调用亡灵类怪物相关的成员函数
		}

	public:
		void attack_enemy_element(M_Element* pobj) //攻击元素类怪物
		{
			//进行攻击处理......
			pobj->getinfo(); //可以调用元素类怪物相关的成员函数
		}

		//其他代码略......
	};
}
(2)调用逻辑与问题

main函数中调用时,需直接创建具体怪物对象并调用对应攻击接口:

_nmsp1::M_Undead* pobjud = new _nmsp1::M_Undead();
_nmsp1::F_Warrior* pobjwar = new _nmsp1::F_Warrior();
pobjwar->attack_enemy_undead(pobjud); // 攻击亡灵怪物

_nmsp1::M_Element* pobjelm = new _nmsp1::M_Element();
pobjwar->attack_enemy_element(pobjelm); // 攻击元素怪物

核心问题

  • 高层组件(F_Warrior)直接依赖低层组件(M_UndeadM_Element),耦合度极高。
  • 新增怪物类型(如机械类M_Mechanic)时,需为F_Warrior新增对应的攻击接口(如attack_enemy_mechanic),违背开闭原则,且代码冗余、扩展性差。

1.2.2遵循 DIP 的重构写法(_nmsp2命名空间)

(1)抽象层定义(Monster类)

先定义所有怪物的抽象父类,作为依赖的抽象层:

namespace _nmsp2
{
    // 抽象层:所有怪物的父类
	class Monster 
	{
	public:
		virtual void getinfo() = 0; // 纯虚函数,定义统一接口
		virtual ~Monster() {} // 父类析构函数需为虚函数
	};
}

(2)具体低层组件(继承抽象层)

让所有具体怪物类继承抽象层,实现统一接口:

namespace _nmsp2
{
    class Monster //作为所有怪物类的父类(抽象层)
	{
	public:
		virtual void getinfo() = 0; //纯虚函数
		virtual ~Monster() {} //做父类时析构函数应该为虚函数
	};
	class M_Undead :public Monster // 亡灵类怪物
	{
	public:
		virtual void getinfo() override
		{
			cout << "这是一只亡灵类怪物" << endl;
		}
	};

	class M_Element :public Monster // 元素类怪物
	{
	public:
		virtual void getinfo() override
		{
			cout << "这是一只元素类怪物" << endl;
		}
	};

	class M_Mechanic :public Monster // 机械类怪物
	{
	public:
		virtual void getinfo() override
		{
			cout << "这是一只机械类怪物" << endl;
		}
	};
}
(3)高层组件重构(依赖抽象层)

修改F_Warrior类,使其依赖抽象层而非具体实现类,统一攻击接口:

namespace _nmsp2
{
	class F_Warrior
	{
	public:
		// 攻击接口依赖抽象层Monster,而非具体怪物类
		void attack_enemy(Monster* pobj) 
		{
			pobj->getinfo(); // 利用多态调用具体怪物的接口
		}
	};
}
(4)调用逻辑与优势

main函数中调用时,通过抽象层指针指向具体怪物对象,实现业务逻辑:

_nmsp2::Monster* pobjud = new _nmsp2::M_Undead();
_nmsp2::F_Warrior* pobjwar = new _nmsp2::F_Warrior();
pobjwar->attack_enemy(pobjud); // 攻击亡灵怪物

_nmsp2::Monster* pobjelm = new _nmsp2::M_Element();
pobjwar->attack_enemy(pobjelm); // 攻击元素怪物

核心优势

  1. 高层组件(F_Warrior)和低层组件(各类怪物)均依赖抽象层(Monster),解除了直接耦合。
  2. 新增怪物类型时,仅需新增继承Monster的具体类,无需修改F_Warrior的攻击接口,符合开闭原则。
  3. 业务逻辑仅需关注抽象层Monster,无需知晓具体怪物类的细节,降低了维护成本。

1.2.3DIP 在策略模式中的体现

策略模式的实现天然符合依赖倒置原则,以第 4 章策略模式的道具系统为例:

  1. 抽象层ItemStrategy(抽象策略类),定义统一的道具使用接口UseItem
  2. 低层组件ItemStrategy_BXD(补血丹)、ItemStrategy_DHD(大还丹)等具体策略类,继承抽象层并实现接口。
  3. 高层组件Fighter(战斗者类,环境类),依赖抽象策略类ItemStrategy,而非具体策略子类。

当新增补血道具时,只需新增具体策略子类,无需修改Fighter类的逻辑,既遵循 DIP,也满足开闭原则。

1.2.4依赖倒置原则的核心意义

  1. 依赖关系倒置:传统结构化编程是 “高层依赖低层”,DIP 要求 “高低层均依赖抽象”,实现了依赖方向的倒置。
  2. 面向接口 / 抽象编程:核心是不针对具体实现类编程,而是针对抽象层编程,提升代码的灵活性和扩展性。
  3. 解耦核心手段:通过抽象层隔离高层与低层的直接关联,使得低层组件的变更不会影响高层业务逻辑。

参考资料来源:王健伟

posted @ 2025-12-12 15:55  CodeMagicianT  阅读(0)  评论(0)    收藏  举报