一、什么是设计模式?
设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的、特定问题的解决方案。
简而言之:解决问题的固定套路。
慎用设计模式。不是所有场景都需要设计模式。
设计模式从何而来?
满足设计原则以后,不断迭代出来的。
设计模式解决什么问题?
具体的需求具有稳定点 又有变化点。如果全是稳定点或全是变化点,就没有必要使用设计模式。
核心目标:期望修改少量的代码,就可以适应需求的变化。
比喻:整洁的房间里有一只好动的猫,怎么保证房间的整洁?把猫关在笼子里。设计模式就是那个"笼子"------把变化点封装起来。
二、设计模式的基础:面向对象思想
2.1 封装
隐藏实现细节,实现模块化。调用者不需要知道内部怎么实现,只需要知道接口怎么用。
2.2 继承
无需修改原有类的情况下,通过继承实现对功能的扩展。
2.3 多态
- 静态多态:函数重载,编译时确定
- 动态多态:继承中虚函数重写,运行时确定
关键机制:如果类中有虚函数,会生成虚函数表指针(vptr),放在对象的最前面。子类也有自己的虚函数指针。
cpp
Base *p = new Subject; // 基类指针指向子类对象
- 普通继承(无虚函数):早绑定,编译时确定调用哪个函数
- 有虚函数:晚绑定(动态多态),运行时根据p实际指向的对象类型调用对应函数
三、设计原则
设计原则是多人总结的经验,会有概念重叠。
3.1 依赖倒置原则
- 实现依赖接口
- 客户也应该依赖接口
- 高层模块不应该依赖底层模块,两者都应该依赖抽象
- 抽象不依赖具体实现,具体实现应该依赖抽象
3.2 开闭原则
对扩展开放,对修改关闭。针对封装和多态,面向接口编程。
3.3 单一职责原则
一个类的职责不要太多。职责越多,变化的可能性越大。
3.4 里氏替换原则
多态。子类可以在需要父类的地方替换父类,且不影响程序正确性。
3.5 接口隔离原则
- 类与类的依赖通过接口隔离
- 组合优于继承
- 最小知道原则:只依赖需要的东西
3.6 封装变化点
两个类依赖越少越好,变化点尽量不要修改。
四、如何学习设计模式?
- 在现有设计模式的基础上扩展代码
- 做功能抽象
- 如何选择设计模式
学习步骤:
- 具体的设计模式解决了什么问题?稳定点?变化点?
- 代码结构是什么?
- 符合哪些设计原则?
- 如何在上面扩展代码?
- 该设计模式有哪些典型应用场景?
- 联系工作场景和开源框架
五、模板方法模式(使用最频繁)
5.1 定义
定义一个操作中的算法的骨架,将一些步骤延迟到子类当中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
5.2 解决什么问题
- 稳定点:算法的骨架(Show方法的整体流程)
- 变化点:子流程需要变化(Show0、Show1等具体实现)
5.3 背景
某个品牌动物园有一套固定的表演流程,但其中若干个表演子流程可以创新替换,以尝试迭代更新表演流程。
5.4 反面示例(不符合设计原则)
cpp
class ZooShow {
public:
ZooShow(int type = 1) : _type(type) {}
// ==========================================
// 【反面示例】:所有逻辑都在一个类里
// 违反原则:开闭原则、单一职责
// ==========================================
void Show() {
if (Show0()) PlayGame(); // 固定流程
Show1(); // 固定流程
Show2(); // 固定流程
Show3(); // 固定流程
}
private:
void PlayGame() {
cout << "after Show0, then play game" << endl;
}
// ==========================================
// 【反面示例】:if-else处理不同类型
// 违反原则:开闭原则
// 每次新增一种type,都需要修改这个方法
// ==========================================
bool Show0() {
if (_type == 1) {
// 类型1的处理逻辑
return true;
}
else if (_type == 2) {
// 类型2的处理逻辑
return false;
}
else if (_type == 3) {
// 类型3的处理逻辑
return false;
}
cout << _type << endl;
return false;
}
void Show1() { /*类型1的show1*/ }
void Show2() { /*类型1的show2*/ }
void Show3() { /*类型1的show3*/ }
int _type; // 存储类型,变化点分散在代码中
};
问题分析:
| 问题 | 违反原则 | 后果 |
|---|---|---|
| if-else处理type | 开闭原则 | 新增type必须修改Show0() |
| 多个子流程在同一个类 | 单一职责 | 变化点太多,牵一发动全身 |
| 子流程是private | 里氏替换 | 子类无法复写这些方法 |
5.5 正确写法:模板方法模式
cpp
// ==========================================
// 模板方法模式
// 【设计原则体现】
// 1. 开闭原则:对扩展开放(子类可复写),对修改关闭(不改动骨架)
// 2. 单一职责:每个虚函数职责单一,骨架只管调用
// 3. 里氏替换:子类对象可替换父类指针使用
// 4. 依赖倒置:使用者依赖抽象接口(Show0等),不依赖具体实现
// ==========================================
class ZooShow {
public:
ZooShow(int type = 1) : _type(type) {}
// ==========================================
// 【稳定点】:算法的骨架,整个流程固定不变
// 模板方法:定义算法骨架,延迟某些步骤到子类
// ==========================================
void Show() {
// Show0是变化点,用虚函数让子类决定
if (Show0()) {
PlayGame(); // 固定逻辑,不变化
}
Show1(); // 变化点,虚函数
Show2(); // 变化点,虚函数
Show3(); // 变化点,虚函数
}
private:
// ==========================================
// 【固定逻辑】:不变化,私有方法
// 封装性:PlayGame是内部固定逻辑,对外不可见
// ==========================================
void PlayGame() {
cout << "after Show0, then play game" << endl;
}
protected:
// ==========================================
// 【变化点】:用protected virtual暴露给子类
//
// protected:对子类可见,对外部不可见
// 符合"最小知道原则",调用者不需要知道这些方法
//
// virtual:允许子类复写,实现动态多态
// 子类不改变算法结构,只改变特定步骤
//
// default返回值:提供默认实现,子类可以不复写
// ==========================================
// 变化点1:开场表演是否有效
virtual bool Show0() {
if (_type == 1) {
// 类型1的判断逻辑
return true;
}
else if (_type == 2) {
// 类型2的判断逻辑
return false;
}
return false;
}
// 变化点2、3、4:具体表演内容
virtual void Show1() {
// 默认实现:空的,子类可选复写
}
virtual void Show2() {
// 默认实现:空的,子类可选复写
}
virtual void Show3() {
// 默认实现:空的,子类可选复写
}
int _type; // 基础类型,基类需要
// ==========================================
// 【接口隔离】:基类只暴露需要的方法给子类
// private:固定逻辑,对外不可见
// protected:变化点,对子类可见
// 没有不必要的public方法,减少依赖
// ==========================================
};
// ==========================================
// 子类扩展:新增一种表演类型
// 【如何扩展】:继承 + 复写虚函数
// 不需要修改任何基类代码!
// ==========================================
class ZooShowEx10 : public ZooShow {
protected:
// ==========================================
// 复写Show0:新的判断逻辑
// 【里氏替换】:可以用ZooShowEx10替换ZooShow使用
// 【开闭原则】:不修改Show()骨架,只改Show0
// ==========================================
virtual bool Show0() override {
if (!expired) { // 新判断条件:是否过期
return true;
}
return false;
}
};
// ==========================================
// 子类扩展:另一种表演类型
// ==========================================
class ZooShowEx20 : public ZooShow {
protected:
// 复写Show1:新的表演内容
virtual void Show1() override {
cout << "ZooShowEx20: 新的表演1" << endl;
}
// 复写Show2:新的表演内容
virtual void Show2() override {
cout << "ZooShowEx20: 新的表演2" << endl;
}
};
5.6 算法骨架流程(文字版)
┌─────────────────────────────────────────────────────────────┐
│ Show() 骨架 │
│ (稳定点:整个表演流程固定,任何子类都不能改变这个流程) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Show0() │──返回true──→ ┌──────────────┐ │
│ │ (变化点:开场) │ │ PlayGame() │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Show1() │──子类可复写(变化点) │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Show2() │──子类可复写(变化点) │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Show3() │──子类可复写(变化点) │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
5.7 代码结构与设计原则对照
cpp
class ZooShow {
// ====== 单一职责 ======
// Show()只负责流程调度,不管具体实现
void Show(); // 公开接口,调用者使用
private:
void PlayGame(); // 私有,只负责固定逻辑
protected:
// ====== 封装变化点 ======
// 变化点用protected暴露给子类,符合"最小知道原则"
// 外部不需要知道这些方法存在
virtual bool Show0();
virtual void Show1();
virtual void Show2();
virtual void Show3();
};
| 设计原则 | 如何体现 |
|---|---|
| 开闭原则 | 扩展新类型只需继承+复写,不修改Show()骨架 |
| 单一职责 | Show()只管流程,变化点分散到各个虚函数 |
| 里氏替换 | ZooShowEx10对象可赋值给ZooShow*指针 |
| 依赖倒置 | 调用者依赖ZooShow接口,不依赖具体子类 |
| 接口隔离 | 只暴露必要的protected方法给子类 |
| 封装变化点 | 变化点用protected virtual,不暴露给外部 |
5.8 使用示例
cpp
// ==========================================
// 客户代码如何使用模板方法模式
// 【依赖倒置】:只依赖抽象接口ZooShow*
// 不需要知道具体是ZooShowEx10还是ZooShowEx20
// ==========================================
void ProcessZooShow(ZooShow* zoo) {
// ==========================================
// 多态调用:实际执行的是哪个子类的Show0/Show1/Show2/Show3
// 由运行时决定,调用者不需要关心
// 【里氏替换】+【动态多态】
// ==========================================
zoo->Show(); // 骨架固定,但子流程由子类决定
}
int main() {
ZooShow base; // 基类
ZooShowEx10 ex10; // 子类1
ZooShowEx20 ex20; // 子类2
ProcessZooShow(&base); // 调用基类的实现
ProcessZooShow(&ex10); // 调用子类1的实现
ProcessZooShow(&ex20); // 调用子类2的实现
// 【开闭原则】:新增类型不需要修改ProcessZooShow
// ZooShowEx30 ex30;
// ProcessZooShow(&ex30); // 直接加一行即可
}
5.9 如何扩展代码?
- 新增子类继承基类ZooShow
- 复写需要变化的方法(Show0/Show1/Show2/Show3)
- 不动任何现有代码,只写新类
典型应用场景:几乎所有项目都会涉及模板方法模式,比如:
- 框架的扩展点设计
- 游戏引擎的技能系统
- 排序算法的骨架(QuickSort的partition是变化点)
六、观察者模式
6.1 定义
定义对象的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象得到通知并自动更新。
6.2 解决什么问题
- 稳定点:一对多的依赖关系,一变化时多跟着变化
- 变化点:观察者数量变化(多增加、多减少)
6.3 背景
气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个(或多个)不同的显示终端。
- 一:数据中心(主题/Subject)
- 多:所有显示终端(观察者/Observer)
6.4 反面示例
cpp
// ==========================================
// 反面示例:显示器直接写死在数据中心里
// 违反原则:开闭原则、单一职责、依赖倒置
// ==========================================
class DisplayA {
public:
// 每个显示器有自己的方法,接口不统一
void Show(float temperature) {
cout << "DisplayA: " << temperature << endl;
}
};
class DisplayB {
public:
void Show(float temperature) {
cout << "DisplayB: " << temperature << endl;
}
};
class DisplayC {
public:
void Show(float temperature) {
cout << "DisplayC: " << temperature << endl;
}
};
class WeatherData {
// 气象数据
};
class DataCenter {
public:
float CalcTemperature() {
WeatherData* data = GetWeatherData();
float temper = data->temperature;
return temper;
}
// ==========================================
// 【反面示例】:显示器写死在内部
// 违反原则:
// 1. 开闭原则:增加新显示器要修改这个类
// 2. 单一职责:DataCenter既要算数据,又要调用显示器
// 3. 依赖倒置:DataCenter依赖具体Display类
// ==========================================
void Update() {
float temper = CalcTemperature();
// 如果要加DisplayD?又要改这里
displayA.Show(temper); // 依赖具体类
displayB.Show(temper); // 依赖具体类
displayC.Show(temper); // 依赖具体类
}
private:
WeatherData* GetWeatherData();
DisplayA displayA; // 直接包含具体类,高层依赖底层
DisplayB displayB;
DisplayC displayC;
};
问题分析:
| 问题 | 违反原则 |
|---|---|
| DataCenter直接持有DisplayA/B/C | 依赖倒置:高层模块依赖底层模块 |
| 增加显示器要改DataCenter | 开闭原则 |
| DataCenter管数据又管显示 | 单一职责 |
| 每个显示器接口不统一 | 接口隔离 |
6.5 正确写法:观察者模式
cpp
#include <list>
using namespace std;
// ==========================================
// 第一步:定义抽象观察者接口
// 【依赖倒置】:所有具体观察者依赖抽象接口,不相互依赖
// 【接口隔离】:只暴露一个方法Show(),最小接口
// ==========================================
class IDisplay {
public:
// ==========================================
// 纯虚函数:观察者必须实现这个接口
// 参数temperature:气象数据
// 【里氏替换】:所有具体观察者可以用IDisplay*使用
// ==========================================
virtual void Show(float temperature) = 0;
virtual ~IDisplay() {} // 虚析构,确保正确清理
};
// ==========================================
// 第二步:实现具体观察者
// 【接口隔离】:只需要实现Show(),不用管别的
// 【单一职责】:每个显示器只管自己的显示
// 【开闭原则】:新增显示器类型不用改现有代码
// ==========================================
class DisplayA : public IDisplay {
public:
virtual void Show(float temperature) override {
cout << "DisplayA show temperature: " << temperature << endl;
}
};
class DisplayB : public IDisplay {
public:
virtual void Show(float temperature) override {
cout << "DisplayB show temperature: " << temperature << endl;
}
};
class DisplayC : public IDisplay {
public:
virtual void Show(float temperature) override {
cout << "DisplayC show temperature: " << temperature << endl;
}
};
// ==========================================
// 第三步:实现主题/被观察者
// 【依赖倒置】:依赖抽象IDisplay*,不依赖具体类
// 【开闭原则】:新增观察者类型不修改这个类
// 【单一职责】:只负责通知,不负责显示
// ==========================================
class WeatherData {
public:
// ==========================================
// 添加观察者
// 【封装变化点】:观察者数量变化在这里封装
// 调用者通过Attach增加,不影响核心逻辑
// ==========================================
void Attach(IDisplay* ob) {
obs.push_back(ob); // 放入观察者列表
}
// ==========================================
// 移除观察者
// 【封装变化点】:观察者减少也在这里封装
// ==========================================
void Detach(IDisplay* ob) {
obs.remove(ob); // 从列表中移除
}
// ==========================================
// 通知所有观察者
// 【稳定点】:通知机制固定不变
// 遍历所有观察者,调用它们的Show()
// ==========================================
void Notify() {
// 1. 获取数据(稳定点)
float temper = CalcTemperature();
// 2. 遍历通知所有观察者(稳定点)
// 【动态多态】:iter可能是DisplayA/DisplayB/DisplayC
// 具体调用哪个,由运行时决定
for (auto iter : obs) {
iter->Show(temper); // 调用抽象接口
}
}
private:
// ==========================================
// 获取气象数据(私有,不暴露给外部)
// ==========================================
WeatherData* GetWeatherData() {
// 实际项目中从气象站获取数据
return nullptr;
}
// ==========================================
// 计算温度(私有内部逻辑)
// ==========================================
float CalcTemperature() {
WeatherData* data = GetWeatherData();
float temper = data->temperature;
return temper;
}
// ==========================================
// 观察者列表
// 【封装变化点】:用list存储,支持动态增删
// 【面向接口编程】:存储IDisplay*,不关心具体类型
// ==========================================
list<IDisplay*> obs; // 观察者列表,可以随时增删
};
6.6 结构图(文字版)
┌─────────────────────────────────────────────────────────────────┐
│ WeatherData (主题/被观察者) │
│ 【稳定点】:Notify()通知机制固定不变 │
│ 【变化点】:观察者数量可以动态增减 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Attach(IDisplay*) ───→ 添加观察者 │
│ Detach(IDisplay*) ───→ 移除观察者 │
│ Notify() ───→ 通知所有观察者 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ list<IDisplay*> obs (观察者列表) │ │
│ │ │ │
│ │ ↓ ↓ ↓ │ │
│ │ [DisplayA*] [DisplayB*] [DisplayC*] │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ Show(float) Show(float) Show(float) │ │
│ │ (具体实现) (具体实现) (具体实现) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
通知流程
───────
气象数据变化 ──→ WeatherData::Notify()
│
▼
┌─────────────┐
│ for (obs) │ ← 稳定点:遍历所有观察者
└─────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
DisplayA DisplayB DisplayC
Show(t) Show(t) Show(t)
(变化点) (变化点) (变化点)
6.7 代码与设计原则对照
| 代码位置 | 设计原则 | 说明 |
|---|---|---|
IDisplay抽象接口 |
依赖倒置 | 具体观察者依赖接口,不相互依赖 |
IDisplay*存储 |
依赖倒置 | WeatherData依赖IDisplay,不依赖DisplayA |
Show(float)=0 |
接口隔离 | 最小接口,只暴露一个方法 |
DisplayA::Show() |
单一职责 | 每个类只管自己的显示 |
Attach/Detach |
封装变化点 | 观察者增删逻辑封装 |
Notify()遍历 |
稳定点 | 通知机制不变 |
list<IDisplay*> |
开闭原则 | 增加新观察者不改WeatherData |
6.8 使用示例
cpp
int main() {
// ==========================================
// 创建主题(被观察者)
// ==========================================
WeatherData weatherData;
// ==========================================
// 创建具体观察者
// 【依赖倒置】:都抽象成IDisplay*
// ==========================================
DisplayA displayA;
DisplayB displayB;
DisplayC displayC;
// ==========================================
// 订阅:把观察者注册到主题
// 【变化点】:可以随时增加或减少观察者
// ==========================================
weatherData.Attach(&displayA);
weatherData.Attach(&displayB);
weatherData.Attach(&displayC);
// ==========================================
// 通知:气象数据更新时,自动通知所有观察者
// 【动态多态】:调用DisplayA/B/C的Show()
// ==========================================
weatherData.Notify();
// 输出:
// DisplayA show temperature: 25.5
// DisplayB show temperature: 25.5
// DisplayC show temperature: 25.5
// ==========================================
// 取消订阅:移除某个观察者
// 【变化点】:观察者减少,不影响其他观察者
// ==========================================
weatherData.Detach(&displayB);
// 再次通知,只有A和C会收到
weatherData.Notify();
// 输出:
// DisplayA show temperature: 26.0
// DisplayC show temperature: 26.0
}
6.9 如何扩展代码?
增加观察者:
- 继承IDisplay,实现Show()方法
- 调用Attach()注册到主题
减少观察者:
- 调用Detach()从主题移除
典型应用场景:
- MVC架构中的Model和View
- GUI事件系统
- 发布-订阅系统
- 消息推送系统
七、策略模式
7.1 定义
定义一系列算法,把它们一个一个封装起来,并且可以使他们互相替换。该模式使得算法可以独立于使用它的客户程序而变化。
7.2 解决什么问题
- 稳定点:客户程序调用具体接口的调用关系
- 变化点:有很多不同的算法,算法内容会改变
7.3 背景
某商场有一个固定促销活动,为了加大促销力度,需要提升国庆节促销活动规格。
不用策略模式的写法:大量if-else判断不同促销类型。
7.4 反面示例
cpp
class Promotion {
public:
double CalcPromotion(double price, int type) {
// ==========================================
// 【反面示例】:if-else处理不同促销
// 违反原则:开闭原则、单一职责
// ==========================================
if (type == 1) {
// 春节促销算法
return price * 0.8;
}
else if (type == 2) {
// 七夕促销算法
return price * 0.85;
}
else if (type == 3) {
// 国庆促销算法
return price * 0.75;
}
// 新增促销类型?又要改这里!
return price;
}
};
问题:
- 新增促销类型要修改这个类
- 算法逻辑和调用逻辑混在一起
- 不容易单独测试某个算法
7.5 正确写法:策略模式
cpp
class Context {
// ==========================================
// 上下文信息:存放计算促销需要的数据
// 【封装】:把相关数据打包在一起
// ==========================================
public:
double price; // 商品价格
int userLevel; // 用户等级
int itemCount; // 商品数量
// 其他需要的上下文数据...
};
// ==========================================
// 第一步:定义抽象策略接口
// 【依赖倒置】:所有具体策略依赖抽象接口
// 【接口隔离】:只暴露一个方法CalcPro()
// ==========================================
class Pstrategy {
public:
// ==========================================
// 纯虚函数:策略必须实现这个接口
// 参数Context:上下文信息,包含价格等数据
// 返回值:折后价格
// ==========================================
virtual double CalcPro(const Context& ctx) = 0;
virtual ~Pstrategy() {}
};
// ==========================================
// 第二步:实现具体策略
// 【单一职责】:每个策略只管自己的算法
// 【开闭原则】:新增策略不修改现有代码
// ==========================================
// 春节促销策略
class VAC_Spring : public Pstrategy {
public:
virtual double CalcPro(const Context& ctx) override {
// ==========================================
// 春节促销算法:满100减20,再打9折
// 【变化点】:算法内容在这里实现
// ==========================================
double price = ctx.price;
if (price >= 100) {
price -= 20; // 满100减20
}
return price * 0.9; // 再打9折
}
};
// 七夕促销策略
class VAC_Qixi : public Pstrategy {
public:
virtual double CalcPro(const Context& ctx) override {
// ==========================================
// 七夕促销算法:全场8折,情侣再减10
// 【变化点】:不同的算法逻辑
// ==========================================
double price = ctx.price * 0.8; // 全场8折
if (ctx.userLevel >= 2) { // 情侣会员再减10
price -= 10;
}
return price;
}
};
// 国庆促销策略
class VAC_NationalDay : public Pstrategy {
public:
virtual double CalcPro(const Context& ctx) override {
// ==========================================
// 国庆促销算法:阶梯折扣
// 【变化点】:更复杂的算法
// ==========================================
double price = ctx.price;
if (ctx.itemCount >= 3) {
// 买3件以上打7折
return price * 0.7;
}
else if (ctx.itemCount >= 2) {
// 买2件打8折
return price * 0.8;
}
// 买1件打9折
return price * 0.9;
}
};
// ==========================================
// 第三步:实现策略上下文(使用策略的类)
// 【依赖倒置】:依赖抽象Pstrategy*,不依赖具体策略
// 【组合优于继承】:通过组合使用策略,不是继承
// ==========================================
class Promotion {
public:
// ==========================================
// 构造函数注入:把策略传进来
// 【依赖注入】:解决类之间的依赖关系
// 调用者决定使用哪个策略
// ==========================================
Promotion(Pstrategy* sss) : s(sss) {}
~Promotion() {}
// ==========================================
// 计算促销价格
// 【稳定点】:调用流程固定
// 1. 调用策略的CalcPro
// 2. 返回结果
// ==========================================
double CalcPromotion(const Context& ctx) {
// 【动态多态】:实际调用的是哪个策略
// 由运行时s指向的对象决定
return s->CalcPro(ctx);
}
private:
// ==========================================
// 【依赖抽象】:存储抽象接口指针
// 【封装变化点】:策略可以随时替换
// ==========================================
Pstrategy* s;
};
7.6 结构图(文字版)
┌─────────────────────────────────────────────────────────────────┐
│ 调用者 (Client) │
│ │
│ Context ctx; │
│ ctx.price = 100; │
│ ctx.userLevel = 2; │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Promotion promotion(new VAC_Spring); // 注入策略 │ │
│ │ promotion.CalcPromotion(ctx); │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Promotion (策略上下文) │
│ 【稳定点】:CalcPromotion流程固定 │
│ 【变化点】:策略可以随时替换 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Promotion(Pstrategy* s) ←── 依赖注入 │
│ CalcPromotion(ctx) ←── 调用策略 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Pstrategy* s; (抽象策略指针) │ │
│ │ │ │ │
│ │ │指向 │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │VAC_Spring │ │ VAC_Qixi │ │VAC_National │ │ │
│ │ │(具体策略) │ │ (具体策略) │ │Day(具体策略) │ │ │
│ │ │ 变化点 │ │ 变化点 │ │ 变化点 │ │ │
│ │ │ CalcPro() │ │ CalcPro() │ │ CalcPro() │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
策略替换示例
┌──────────────────────────────────────────────────────────┐
│ │
│ Promotion p1(new VAC_Spring); // 春节策略 │
│ Promotion p2(new VAC_Qixi); // 七夕策略 │
│ Promotion p3(new VAC_NationalDay); // 国庆策略 │
│ │
│ // 同一套Promotion代码,不同策略,不同结果! │
│ // 【开闭原则】:新增策略不修改Promotion类 │
│ │
└──────────────────────────────────────────────────────────┘
7.7 依赖注入详解
cpp
// ==========================================
// 依赖注入:把依赖作为参数传进来
// 好处:调用者决定使用哪个策略,被调用者不需要知道细节
// ==========================================
// 方式1:构造函数注入(上面用的这种方式)
class Promotion {
public:
Promotion(Pstrategy* sss) : s(sss) {} // 构造时注入
private:
Pstrategy* s;
};
// 方式2:设值方法注入
class Promotion {
public:
void SetStrategy(Pstrategy* sss) { s = sss; } // 运行时注入
private:
Pstrategy* s;
};
// 方式3:方法参数注入
class Promotion {
public:
double CalcPromotion(Pstrategy* s, const Context& ctx) {
return s->CalcPro(ctx);
}
};
7.8 代码与设计原则对照
| 代码位置 | 设计原则 | 说明 |
|---|---|---|
Pstrategy抽象接口 |
依赖倒置 | Promotion依赖抽象,不依赖具体 |
Pstrategy* s存储 |
依赖倒置 | 依赖抽象接口指针 |
CalcPro(const Context&) |
接口隔离 | 最小接口,一个方法 |
VAC_Spring::CalcPro() |
单一职责 | 每个策略只管自己算法 |
Attach/Detach |
封装变化点 | 观察者增删逻辑封装 |
Promotion持有策略指针 |
组合优于继承 | 用组合而不是继承使用策略 |
| 新策略继承Pstrategy | 开闭原则 | 扩展不需要修改Promotion |
7.9 使用示例
cpp
int main() {
// ==========================================
// 创建上下文
// ==========================================
Context ctx;
ctx.price = 100;
ctx.userLevel = 2;
ctx.itemCount = 2;
// ==========================================
// 使用不同的策略
// 【开闭原则】:新增策略不修改这段代码
// ==========================================
// 春节促销
VAC_Spring springStrategy;
Promotion promotion1(&springStrategy);
cout << "春节促销价: " << promotion1.CalcPromotion(ctx) << endl;
// 七夕促销
VAC_Qixi qixiStrategy;
Promotion promotion2(&qixiStrategy);
cout << "七夕促销价: " << promotion2.CalcPromotion(ctx) << endl;
// 国庆促销
VAC_NationalDay nationalStrategy;
Promotion promotion3(&nationalStrategy);
cout << "国庆促销价: " << promotion3.CalcPromotion(ctx) << endl;
// ==========================================
// 运行时切换策略
// ==========================================
Promotion promo(new VAC_Spring);
cout << promo.CalcPromotion(ctx) << endl; // 春节价
promo = Promotion(new VAC_Qixi); // 切换为七夕
cout << promo.CalcPromotion(ctx) << endl; // 七夕价
}
7.10 如何扩展代码?
- 新增策略类 继承
Pstrategy - 实现
CalcPro()方法 - 使用时传入新策略
cpp
// 新增:双十一促销策略
class VAC_Double11 : public Pstrategy {
public:
virtual double CalcPro(const Context& ctx) override {
// 双十一算法:定金翻倍
double price = ctx.price;
if (ctx.userLevel >= 1) {
price *= 0.5; // 会员半价
}
return price;
}
};
// 使用:直接替换即可
VAC_Double11 double11;
Promotion promo(&double11);
典型应用场景:
- 支付方式选择(支付宝/微信/银行卡)
- 排序算法选择(快排/归并/堆排)
- 压缩算法选择(zip/rar/7z)
- 出行路线规划(驾车/公交/步行)
八、三大模式对比总结
| 模式 | 核心思想 | 稳定点 | 变化点 | 关键结构 |
|---|---|---|---|---|
| 模板方法 | 继承+虚函数 | 算法骨架 | 子流程 | 父类定义骨架,子类复写虚函数 |
| 观察者 | 一对多通知 | 通知机制 | 观察者数量 | Subject维护观察者列表,Attach/Detach |
| 策略模式 | 算法封装 | 调用关系 | 算法内容 | Context持有Strategy指针,算法可替换 |
共同遵循的设计原则
| 设计原则 | 模板方法 | 观察者 | 策略模式 |
|---|---|---|---|
| 开闭原则 | + | + | + |
| 依赖倒置 | + | + | + |
| 接口隔离 | + | + | + |
| 单一职责 | + | + | + |
| 里氏替换 | + | + | - |
| 封装变化点 | + | + | + |
学习设计模式的方法论
┌─────────────────┐
│ 问题识别 │
│ 稳定点在哪? │
│ 变化点在哪? │
└────────┬────────┘
▼
┌─────────────────┐
│ 选择模式 │
│ 一对多? │
│ 算法不同? │
│ 流程固定? │
└────────┬────────┘
▼
┌─────────────────┐
│ 应用原则 │
│ 依赖抽象 │
│ 封装变化 │
│ 开闭优先 │
└────────┬────────┘
▼
┌─────────────────┐
│ 代码实现 │
│ 接口抽象 │
│ 组合/继承 │
│ 多态调用 │
└─────────────────┘
九、实战建议
何时使用设计模式?
- 存在稳定点+变化点的场景
- 需要频繁扩展功能
- 希望遵循良好的设计原则
何时不用设计模式?
- 简单逻辑能直接解决的问题
- 全是稳定点或全是变化点
- 过早优化反而增加复杂度
核心心法
应对稳定点,通过抽象;应对变化点,通过扩展(继承和组合)。
- 抽象:用接口/基类表达稳定点
- 扩展:用子类/组合实现变化点
面向接口编程,减少系统中各部分的依赖关系,实现"高内聚、松耦合"。
根据零声教育教学写作https://github.com/0voice