第30章 设计模式基础
第30章 设计模式基础
- 设计模式的核心思想与分类
- 三种最常用创建型模式的实现与应用场景
- 如何识别代码中的“坏味道”并应用模式重构
调试日志里频繁出现“内存泄漏”,新加一个功能要修改十几个文件,团队成员抱怨代码像“意大利面条”一样难以理解——如果你遇到过这些问题,说明你的项目架构需要设计模式了。设计模式不是语法,而是针对特定设计问题的、可复用的优秀解决方案模板。
生活类比: 就像乐高积木的通用连接件,不同形状的积木(类)可以通过标准接口(模式)牢固地组合在一起,构建出复杂稳定的结构。技术定义: 设计模式是软件设计中,针对一类反复出现的问题,所总结出的、经过验证的通用解决方案。它描述了对象或类之间的关系,以及它们如何协作完成任务。
设计模式的“家族”与核心价值
GoF(四人帮)的经典著作《设计模式:可复用面向对象软件的基础》归纳了23种模式,分为三大类。理解这个分类,能帮你快速定位问题该用哪个模式解决。
实际开发中,大部分团队会优先掌握其中5-8个最常用的模式。设计模式的核心价值在于:提升代码复用性、增强系统可扩展性、降低模块间耦合度、提高代码可读性。 我在一个图像处理项目中,通过引入工厂模式,将支持新图片格式的开发时间从2天缩短到2小时。
创建型模式:解开对象创建的枷锁
当你的代码中充斥着大量的 new 关键字,并且创建逻辑复杂多变时,创建型模式就该登场了。它封装了对象的创建过程,使系统独立于具体对象的创建、组合和表示。
| 模式名称 | 要解决的问题 | 典型应用场景 | C++实现关键 |
|---|---|---|---|
| 单例模式 (Singleton) | 确保一个类只有一个实例,并提供全局访问点 | 配置管理器、日志记录器、线程池、数据库连接池 | 私有构造函数、静态成员函数、线程安全(C++11后可用局部静态变量) |
| 工厂方法 (Factory Method) | 将对象创建延迟到子类,避免客户端代码依赖具体类 | 文档编辑器创建不同类型的文档(Word, PDF);跨平台UI组件创建 | 定义创建对象的接口,子类决定实例化哪个类 |
| 抽象工厂 (Abstract Factory) | 创建一系列相关或依赖的对象,而不指定其具体类 | 构建不同风格(如Modern/Vintage)的整套UI控件;连接不同数据库(MySQL/Oracle)的整套DAO对象 | 包含多个工厂方法的接口,每个具体工厂生产一套产品 |
单例模式:全局唯一的“管家”
日志系统是单例模式的经典场景。如果每个模块都自己创建日志对象,不仅浪费资源,还可能导致日志文件写入冲突。
// C++11 线程安全的Meyer‘s Singleton实现(推荐)
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // C++11保证局部静态变量初始化线程安全
return instance;
}
void log(const std::string& message) {
// 写入日志文件...
std::cout << "[LOG] " << message << std::endl;
}
// 禁止拷贝和赋值
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() {} // 私有构造函数
~Logger() {}
};
// 使用方式
Logger::getInstance().log("Application started.");
常见误区
- 滥用单例: 单例本质上是“全局变量”,滥用会导致代码高度耦合,难以测试。只有真正需要全局唯一性的“管理器”类才适合。
- 线程安全陷阱: 在C++11之前,使用“双检锁”实现单例很容易写错,导致未定义行为。C++11之后,请统一使用上面展示的局部静态变量方式。
- 忽视销毁: 单例对象在程序结束时析构,如果析构顺序依赖其他全局/静态对象,可能引发问题。设计时要考虑资源清理的顺序。
工厂方法模式:生产线的标准化
假设你正在开发一个支持多种传感器(如温度传感器DHT22、湿度传感器SHT31)的数据采集框架。每次新增传感器类型,都要修改核心采集逻辑,这违反了“开闭原则”。
// 产品接口
class Sensor {
public:
virtual ~Sensor() = default;
virtual float readData() = 0;
};
// 具体产品
class DHT22Sensor : public Sensor { public: float readData() override { /* 读取温度 */ return 25.3; } };
class SHT31Sensor : public Sensor { public: float readData() override { /* 读取湿度 */ return 60.5; } };
// 简单工厂(非严格GoF工厂方法,但更常用)
class SensorFactory {
public:
static std::unique_ptr createSensor(const std::string& type) {
if (type == "DHT22") return std::make_unique();
else if (type == "SHT31") return std::make_unique();
// 新增传感器只需修改这里
return nullptr;
}
};
如果你看到代码里有一长串的 if-else 或 switch-case 来创建对象,这就是引入工厂模式的强烈信号。我在一个通信协议解析项目中踩过这个坑,最初用条件判断创建不同协议解析器,后来协议增加到十几种,代码变得难以维护,改用工厂模式配合注册表后清爽多了。
编者提示: 工厂模式中返回的通常是动态分配的对象。请务必结合第25章《智能指针》的内容,使用 std::unique_ptr 来管理这些对象,避免内存泄漏,并明确所有权语义。
结构型模式:搭建灵活的类关系
结构型模式关注如何将类或对象组合成更大、更复杂的结构,同时保持结构的灵活和高效。这里我们重点看一个极其常用的模式——适配器模式。
生活类比: 就像电源转换插头,让你英标的笔记本电脑插头(客户端)能在欧标的墙插(已有接口)上正常工作。技术定义: 适配器模式将一个类的接口转换成客户期望的另一个接口,使原本因接口不兼容而无法一起工作的类可以协同工作。
假设你项目里有一个遗留的、接口陈旧的第三方日志库 OldLogger,它只有一个 writeToConsole 方法。但你的新系统统一使用一个接收 std::string 参数的 log 接口。
// 已有的、不兼容的类
class OldLogger {
public:
void writeToConsole(const char* msg, int length) {
std::cout.write(msg, length);
}
};
// 目标接口(你的新系统标准)
class NewLoggerInterface {
public:
virtual ~NewLoggerInterface() = default;
virtual void log(const std::string& message) = 0;
};
// 适配器类:继承目标接口,内部包含一个OldLogger对象
class LoggerAdapter : public NewLoggerInterface {
private:
OldLogger oldLogger;
public:
void log(const std::string& message) override {
// 适配过程:将string接口转换为char*和length
oldLogger.writeToConsole(message.c_str(), message.size());
}
};
// 客户端代码可以统一使用NewLoggerInterface接口,无需知道OldLogger的存在
std::unique_ptr logger = std::make_unique();
logger->log("Hello, Adapter Pattern!");
行为型模式:对象间的智慧协作
行为型模式负责对象间的高效通信和职责分配。观察者模式是其中使用频率最高的模式之一,它定义了对象间的一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知并自动更新。
GUI事件处理
- 按钮(被观察者)被点击
- 多个对话框、状态栏(观察者)自动更新
数据模型变更
- 股票价格(被观察者)变化
- 多个图表视图(观察者)实时重绘
游戏引擎
- 玩家生命值(被观察者)减少
- UI血条、音效系统、成就系统(观察者)触发响应
下面是一个简化的C++实现框架,注意使用弱引用来避免观察者对象生命周期管理带来的问题。
#include
#include
#include
// 前向声明
class Observer;
// 被观察者(主题)
class Subject {
std::vector> observers_; // 使用weak_ptr避免循环引用
std::string state_;
public:
void attach(std::weak_ptr obs) { observers_.push_back(obs); }
void setState(const std::string& newState) {
state_ = newState;
notify();
}
void notify(); // 通知所有观察者
std::string getState() const { return state_; }
};
// 观察者接口
class Observer : public std::enable_shared_from_this {
public:
virtual ~Observer() = default;
virtual void update(Subject* subject) = 0;
};
void Subject::notify() {
for (auto& weakObs : observers_) {
if (auto obs = weakObs.lock()) { // 检查观察者是否还存在
obs->update(this);
}
}
}
// 具体观察者
class ConcreteObserver : public Observer {
std::string name_;
public:
ConcreteObserver(const std::string& name) : name_(name) {}
void update(Subject* subject) override {
std::cout << name_ << " received update. New state: " << subject->getState() << std::endl;
}
};
关键点:何时使用设计模式? 不要为了用模式而用模式。当你发现代码有“坏味道”,如:重复代码太多、类职责过重、修改一处牵动全身、难以添加新功能时,再回头查阅设计模式手册,寻找对应的解决方案。模式是“重构”的目标,而不是初始设计的起点。
动手试一试
- 改造日志系统: 你有一个简单的控制台日志类 ConsoleLogger。请为其实现一个单例模式,并添加一个静态方法 setLogLevel(Level) 来全局控制日志输出级别(如DEBUG, INFO, ERROR)。
- 实现一个图形工厂: 定义一个 Shape 抽象类,包含 draw() 和 area() 纯虚函数。创建 Circle 和 Rectangle 两个派生类。最后编写一个 ShapeFactory,根据传入的字符串 “Circle” 或 “Rectangle” 返回对应的形状对象。
- 模拟简单的事件通知: 创建一个 WeatherStation(被观察者)类,它有一个温度数据成员。创建两个观察者:Display(在温度变化时打印信息)和 Logger(在温度超过30度时记录警告)。实现观察者模式,当气象站更新温度时,两个观察者能自动响应。
检验你的理解
- 单例模式的构造函数为什么必须是私有的?如果公开会有什么后果?
- 工厂方法模式和抽象工厂模式最主要的区别是什么?请各举一个适合使用该模式的例子。
- 在观察者模式的实现中,为什么存储观察者时推荐使用 std::weak_ptr 而不是 std::shared_ptr?
- 假设你有一个已有的、功能完善的类,但其接口不符合新系统的要求。在不修改原有类代码的前提下,你可以使用哪种设计模式来让它融入新系统?
本章小结
- 设计模式是针对常见软件设计问题的、可复用的解决方案模板,分为创建型、结构型和行为型三大类。
- 单例模式确保一个类仅有一个实例,C++11后推荐使用局部静态变量实现线程安全版本。
- 工厂模式(工厂方法/抽象工厂)将对象创建逻辑封装起来,使客户端代码与具体类解耦,极大提升了系统的可扩展性。
- 适配器模式像转换插头一样,使接口不兼容的类能够一起工作,常用于集成遗留代码或第三方库。
- 观察者模式实现了一对多的依赖关系,是事件驱动系统、MVC架构的核心,实现时需注意使用弱引用来管理观察者生命周期。
- 学习设计模式的正确姿势是:先识别代码中的“坏味道”,再寻找对应的模式进行重构,而非在项目初期生搬硬套。
微信扫码关注
抖音扫码关注