【读书笔记】《C++ Software Design》第十章与第十一章 The Singleton Pattern The Last Guideline
《C++ Software Design》第十章与第十一章 The Singleton Pattern & The Last Guideline
在很多开发者心中,Singleton(单例模式) 是设计模式的经典代表,广泛用于日志系统、配置中心、资源管理等场景。然而,《C++ Software Design》的第十章提醒我们:Singleton 其实更像是实现技巧而非架构思想。本章围绕单例的设计问题展开批判性分析,并提供更现代、可测试、易维护的替代方案。
第十一章作为本书收尾,对学习设计模式提出重要建议:设计模式不是目标,而是通向架构思维的工具。
第十章:重新审视 Singleton 模式的使用与替代
Guideline 37:将 Singleton 视为实现细节,而非设计理念
什么是 Singleton?
Singleton 的目标是:确保类在系统中只有一个实例,并提供全局访问点。
经典实现如下:
class Logger {
public:static Logger& instance() {static Logger inst;return inst;}void log(const std::string& msg) {std::cout << "[LOG] " << msg << "\n";}private:Logger() = default;
};
调用方式:
Logger::instance().log("System started.");
听起来没问题?其实隐藏了重大的设计隐患。
Singleton 的本质问题
“Singleton 模式不是用来解决依赖问题的,而是绕开依赖注入的。”
主要缺陷:
- 隐式依赖:你无法从函数签名看出它是否依赖单例;
- 不可测试:单例通常无法替换为 mock 或 fake;
- 状态污染:多个测试用例运行时共享状态,容易出现隐式耦合;
- 生命周期不受控:程序难以在特定阶段“重置”或清除全局状态;
- 难以组合:与其它策略模式、配置体系耦合度高。
Guideline 38:设计可替代、可测试的 Singleton
认识本质:Singleton 是全局状态
如果你把单例对象当作状态容器,那么你就会意识到它带来的是“全局共享变量的封装体”。
class ConfigManager {
public:std::string get(const std::string& key);
};
本质上你在访问一个静态变量,只不过套了个类。程序越大,越容易失控。
Singleton 如何阻碍可变性与可测试性?
例子:
class Authenticator {
public:bool login(const std::string& user, const std::string& pass) {Logger::instance().log("User login attempt.");// ...}
};
问题在于:
- 测试
Authenticator
时不能 mock 掉Logger
- 甚至不能确保 log 被正确写入
- 需要手动重置状态、捕获输出,非常脆弱
更好的做法:反转依赖 + 局部注入
将 Logger
从单例转换为可替换依赖:
class ILogger {
public:virtual void log(const std::string&) = 0;virtual ~ILogger() = default;
};class ConsoleLogger : public ILogger {
public:void log(const std::string& msg) override {std::cout << msg << "\n";}
};class Authenticator {ILogger& logger;
public:Authenticator(ILogger& l) : logger(l) {}bool login(const std::string& user, const std::string& pass) {logger.log("Login attempt");return true;}
};
- 使用接口(Strategy 模式)
- 支持注入任意实现(如 MockLogger)
- 不再依赖全局状态
局部注入的迁移策略
- 识别所有对单例的使用点
- 将单例改为接口(如
ILogger
) - 替换调用点为构造注入、函数参数注入
- 最后完全移除 Singleton 实现,只保留实例化层使用
这就是“从 Singleton 走向依赖注入”的过程。
第十一章:继续学习设计模式的建议
Guideline 39:持续学习设计模式,别止步于列表记忆
不要将设计模式当作 API 说明书
很多开发者学习设计模式是为了“面试背题”或“记住几个经典结构”。但真实的软件工程场景中:
- 设计模式应当被灵活组合
- 模式之间存在转换关系与融合
- 每种模式都依赖于语言、语义、场景的适配
模式 ≠ 静态结构,模式 = 可演化的语言
设计模式是人们用来交流复杂架构思想的“语言工具”:
模式 | 本质功能 | 替代表达 |
---|---|---|
Strategy | 行为切换 | 函数对象 / 多态 |
Singleton | 全局共享 + 惰性初始化 | 注入 / 缓存 / SFO |
Decorator | 可组合行为增强 | 模板 / 类型擦除 |
CRTP | 编译期策略注入 | Concepts(C++20) |
Prototype | 运行时复制能力 | clone / value-copy |
Adapter | 接口变换 | Wrapper / Conversion |
你应当思考这些模式如何与 C++ 模板系统、类型系统、运行时模型协作,而非照本宣科地“照搬结构图”。
学习建议
- 阅读现代库源码(如 Boost, Abseil, Folly)掌握实际模式演化
- 关注语言发展,如 C++20 Concepts 如何替代 CRTP
- 使用组合优先替代继承,追求“行为解耦”而非“类关系构造”
- 编写可测试、可演化的架构代码,让设计模式成为服务于变更的工具
总结
内容核心 | 关键要点 |
---|---|
Singleton 是实现细节,不是架构主张 | 它隐藏依赖、妨碍测试、耦合状态 |
更好的方式是依赖注入和接口分离 | 用 Strategy 模式组合替代 |
模式之间应灵活组合、替换、转化 | 不应生搬硬套传统结构图 |
学习设计模式是设计思维的启蒙 | 最终目标是写出可演化的系统 |
架构思维
不要被模式束缚,而是要用模式服务于可维护性与系统演化
从设计单例到重构为依赖注入,从背诵模式结构图到理解它们的演化语义,我们才能真正迈入“架构设计”的世界。
设计模式不是答案,而是探索的起点。