【读书笔记】《C++ Software Design》第一章《The Art of Software Design》
本章通过五项核心指导原则(Guidelines)详细阐述 C++ 软件设计的艺术,结合具体示例和实战建议,提供落地可行的最佳实践。
Guideline 1: Understand the Importance of Software Design
1.1 Features Are Not Software Design
-
说明:功能实现(Feature)侧重于使软件满足需求,而设计关注系统的整体质量与可演化性。
-
具体展开:
- 示例对比:假设两个矩阵乘法模块A和B,A直接嵌套三层循环,B通过分块(blocking)优化、面向接口编写、支持并行策略参数化。两者都能"乘矩阵",但B在性能扩展、单元测试和替换算法时显著优于A。
- 影响:A模块尽管功能正确,却因高耦合、不易替换、缺乏抽象导致后续维护成本极高。
1.2 Software Design: The Art of Managing Dependencies and Abstractions
-
依赖管理:
-
依赖倒置:将高层模块依赖于抽象接口(纯虚类或模板概念),如下示例:
cppstruct ILogger { virtual void log(const std::string&) = 0; }; class FileLogger : public ILogger { /* ... */ }; class Processor { std::unique_ptr<ILogger> logger_; public: Processor(std::unique_ptr<ILogger> l): logger_(std::move(l)){} void process(){ logger_->log("start"); /*...*/ } };
-
效果:业务逻辑与具体日志实现解耦,可在运行或编译时替换不同 Logger。
-
-
抽象层次:
-
分层架构:界面层、业务层、数据层;每层仅通过接口通信。示例:
- DAO 模块暴露
IDataAccess
接口 - Service 模块仅持有
std::shared_ptr<IDataAccess>
- UI 模块调用 Service,无需知道底层实现。
- DAO 模块暴露
-
1.3 The Three Levels of Software Development
-
Level 1: Feature Delivery(功能交付)
- 特点:快速响应需求,无严格设计;常见于初版或 PoC。
- 弊端:代码结构混乱、难以测试、难以扩展。
-
Level 2: Engineering Excellence(工程卓越)
- 特点:引入流程(CI/CD、代码审查)、工具(静态分析、单元测试)。
- 实践:使用 Git Hooks 强制代码风格;配置 CMake 脚本自动运行 Google Test。
-
Level 3: Design Mastery(设计掌握)
- 特点:深度应用设计原则与模式,关注长期可演化。
- 实践示例 :在插件框架中引入基于
dlopen
的动态加载,并通过工厂模式和反射技术实现模块自动注册。
1.4 The Focus on Features
- 问题现象:项目初期以功能为中心,业务变化后却发现模块难以拆分与替换。
- 案例详解:电商系统早期将支付逻辑直接写在订单处理流程中,后续接入新支付渠道需要修改大量订单代码。
- 解决方案 :将支付逻辑提取到
IPaymentProcessor
接口,并用策略模式封装不同渠道,实现无感切换。
1.5 The Focus on Software Design and Design Principles
-
SOLID 在 C++ 中实战:
- Single Responsibility :每个类仅关注一项职责,以
= delete
禁用不相关构造。 - Open/Closed:通过 CRTP 或模板特化,在不改动原类的情况下扩展功能。
- Liskov Substitution:子类覆盖时保证前置条件不加强、后置条件不削弱。
- Single Responsibility :每个类仅关注一项职责,以
-
KISS 与 YAGNI:仅在需求明确时抽象新模块,避免过早引入复杂框架。
-
DRY:利用模板或宏消除重复,如对相似算法提炼为模板函数。
-
Separation of Concerns:UI、逻辑、存储分层;示例代码详见附录A。
Guideline 2: Design for Change
2.1 Separation of Concerns
-
定义:将系统功能划分为相对独立的模块,每个模块专注单一职责。
-
实践:使用命名空间、库分割和 CMake target 实现物理分层。
-
示例:图像处理管道分为:
- 格式解析模块
- 像素变换模块
- 编码输出模块
每一模块与其他仅通过纯函数或接口交互。
2.2 An Example of Artificial Coupling
- 场景:订单服务依赖用户服务、库存服务,但实际业务仅需调用库存接口。
- 问题:直接包含用户头文件引入不必要依赖,导致编译联动。
- 重构 :引入
IInventory
接口,将与用户相关的聚合逻辑移至外部 Adapter。
2.3 Logical Versus Physical Coupling
- 逻辑耦合:概念依赖,通过接口解耦后仍需关注功能调用顺序。
- 物理耦合:编译时依赖,通过前向声明、PImpl 等技术消除。
- 示例:使用 PImpl 隐藏私有成员,减少头文件依赖。
2.4 Don't Repeat Yourself
- 实例:多个模块都有类似的配置加载与验证逻辑。
- 重构前 :各自实现
loadConfig()
方法,代码重复。 - 重构后 :提取
ConfigLoader<T>
模板类,支持所有模块配置加载,消除冗余。
2.5 Avoid Premature Separation of Concerns
- 警示:过早抽象会导致不必要的复杂接口。
- 衡量准则 :当有至少两个模块出现相同代码时再抽象;使用
grep
或代码度量工具辅助决策。
Guideline 3: Separate Interfaces to Avoid Artificial Coupling
3.1 Segregate Interfaces to Separate Concerns
-
接口隔离原则(ISP):将大接口拆分为多个小接口,避免客户端依赖不必要的方法。
-
C++ 示例:
cppstruct IReadable { virtual std::string read() = 0; }; struct IWritable { virtual void write(const std::string&) = 0; }; struct IReadWrite : IReadable, IWritable {};
-
效果 :读取组件仅实现
IReadable
,写入组件仅实现IWritable
,减少耦合。
3.2 Minimizing Requirements of Template Arguments
-
问题:过度依赖模板参数的成员函数会导致难以重用。
-
最佳实践:
- 使用 C++20 Concepts 明确约束,如
template<Readable R> void func(R& r)
。 - 对功能特征进行分层接口,避免单个模板参数承担过多职责。
- 使用 C++20 Concepts 明确约束,如
-
示例 :实现通用
serialize
模板,仅依赖toString()
方法,而非整个对象。
Guideline 4: Design for Testability
4.1 How to Test a Private Member Function
-
常见做法:
- 使用
friend
声明测试类访问私有成员。 - 通过宏映射将私有方法编译为公共。
- 使用
-
弊端:破坏封装。
-
示例:
cppclass Foo { private: int compute(); friend class FooTest; };
4.2 The True Solution: Separate Concerns
-
核心思路:将复杂逻辑从私有方法中抽离到独立类或函数,保证可直接测试。
-
示例重构:
-
原始
Foo::process()
内部含 5 步算法:cppint a = step1(); int b = step2(a); ...
-
重构后:
cppclass Processor { public: int step1(); int step2(int); }; Foo holds Processor; // 在测试中直接 new Processor()
-
-
好处:无需暴露私有成员,即可对每个步骤单元测试。
Guideline 5: Design for Extension
5.1 The Open-Closed Principle
-
定义:对扩展开放,对修改关闭。
-
C++ 实现:
- 虚函数基类 + 派生扩展
- 策略模式:将可变行为封装为策略对象
- 通过插件系统
.so
动态加载新的实现
-
示例:
cppstruct IFormatter { virtual std::string format(int) = 0; }; class JsonFormatter : public IFormatter {/*...*/}; class XmlFormatter : public IFormatter {/*...*/};
5.2 Compile-Time Extensibility
-
模板元编程 :利用模板特化、整型常量、
if constexpr
实现编译期分支。 -
参数化多态:CRTP、Traits 类。
-
示例:
cpptemplate<typename T> struct Serializer; template<> struct Serializer<Json> { static void serialize(...); };
5.3 Avoid Premature Design for Extension
-
问题:过早设计插件机制或策略接口,若只有一个实现反而增加复杂度。
-
建议:
- 根据 YAGNI 原则,仅在第二个实现需求出现时才引入扩展点。
- 使用度量工具跟踪实现数量动态决策。
本章小结
- 软件设计是一门结合科学严谨、工程流程与艺术创意的综合学科。
- 五大指导原则(了解设计、应对变化、隔离接口、可测试性、可扩展性)协同作用。
- 在 C++ 中,灵活运用模板、虚函数、多态、RAII 等特性,是实现高质量设计的关键。
- 实战中,需根据项目规模与团队成熟度,平衡抽象深度与实现复杂度。