【读书笔记】《C++ Software Design》第二章:The Art of Building Abstractions
《C++ Software Design》第二章:The Art of Building Abstractions
本章详细探讨如何在 C++ 中构建高质量的抽象,包括行为预期、一致性、所有权管理和文档化。
Guideline 6: Adhere to the Expected Behavior of Abstractions
6.1 An Example of Violating Expectations
-
示例场景:实现一个
Stack<T>
类时,提供pop()
方法返回T&
,但在空栈调用时未抛出异常,而是返回未定义引用。 -
问题:客户端无法预期
pop()
行为,违反直觉。 -
解决方案:
- 改用
std::optional<T>
返回值,或抛出明确异常std::underflow_error
。 - 添加文档注释,明确表明函数的异常保证级别(noexcept 语义)。
- 改用
6.2 The Liskov Substitution Principle
-
定义:子类型必须能够替换父类型且不改变程序语义。
-
具体实践:
-
前置条件:子类方法不应要求更严格输入。
-
后置条件:子类方法输出应满足基类声明的约定。
-
示例:
struct IShape { virtual double area() const = 0; }; struct Rectangle : IShape { double area() const override; }; struct Square : Rectangle { double side; double area() const override { return side * side; } };
-
确保
Square
中不破坏Rectangle
的预期行为,如修改side
时边长一致性。
-
6.3 Criticism of the Liskov Substitution Principle
- 争议:严格遵循 LSP 可能导致复杂的类型层次和过度抽象。
- 作者观点:注重抽象的语义契约即可,不必机械化实施所有 LSP 条件。
- 实践建议:编写契约测试(Contract Tests)验证继承层次的行为一致性。
6.4 The Need for Good and Meaningful Abstractions
-
要点:抽象应贴合领域概念,避免陷入技术细节。
-
示例:定义
Money
类型:class Money {long cents; public:explicit Money(long c): cents(c) {}double as_dollars() const { return cents / 100.0; } };
-
好处:减少单元转换错误,增强类型安全性与可读性。
Guideline 7: Understand the Similarities Between Base Classes and Concepts
-
继承与概念:两者均定义范畴与行为规范。
-
运行时 vs 编译时:继承多态在运行时决议;概念约束在编译时检查。
-
示例对比:
// 继承多态 struct IFoo { virtual void foo() = 0; }; void callFoo(IFoo* f) { f->foo(); }// 概念约束 template<typename T> concept Fooable = requires(T a) { a.foo(); }; void callFoo(Fooable auto& f) { f.foo(); }
-
实践:对性能敏感场景优先使用概念,需动态扩展场景使用继承。
Guideline 8: Understand the Semantic Requirements of Overload Sets
8.1 The Power of Free Functions: A Compile-Time Abstraction Mechanism
-
优势:支持对第三方类型扩展接口,无需修改原始类。
-
示例:
struct Vector2D { float x, y; }; inline float length(const Vector2D& v) { return std::hypot(v.x, v.y); }
8.2 The Problem of Free Functions: Expectations on the Behavior
-
契约:自由函数应与成员函数语义一致。
-
示例:
std::data()
,std::size()
应返回与成员函数相同结果。 -
实践:
- 在头文件中使用
using std::data;
保证 ADL 正常。 - 文档明确输出和异常语义。
- 在头文件中使用
Guideline 9: Pay Attention to the Ownership of Abstractions
9.1 The Dependency Inversion Principle
-
实践示例:
struct ILogger { virtual void log(std::string_view) = 0; }; class ConsoleLogger : public ILogger { void log(std::string_view msg) override { std::cout<<msg; } }; class App { std::unique_ptr<ILogger> logger; };
9.2 Dependency Inversion in a Plug-In Architecture
-
步骤:
- 定义插件接口
IPlugin
。 - 插件库导出
createPlugin()
C 接口。 - 主程序用
dlopen/dlsym
加载并注册。
- 定义插件接口
-
代码:
extern "C" IPlugin* createPlugin();
9.3 Dependency Inversion via Templates
-
示例:
template<typename Logger> class App { Logger logger; };
9.4 Dependency Inversion via Overload Sets
-
示例:
void save(Data& d) { d.save(); } // 自由函数调用 d.save()
9.5 DIP vs SRP
- DIP:管理高低层依赖。
- SRP:管理类职责。
- 结合:通过细粒度接口同时满足二者。
Guideline 10: Consider Creating an Architectural Document
-
目的:提升团队对系统全貌的理解。
-
建议文档:
- 组件图(Component Diagram)
- 类图(Class/Concept Diagram)
- 序列图(Sequence Diagram)
- 数据流图(Data Flow Diagram)
-
工具:PlantUML、Mermaid。
-
实践:将 UML 源文件纳入版本控制,与代码同步迭代。