【读书笔记】《C++ Software Design》第一章《The Art of Software Design》

【读书笔记】《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

  • 依赖管理

    • 依赖倒置:将高层模块依赖于抽象接口(纯虚类或模板概念),如下示例:

      cpp 复制代码
      struct 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,无需知道底层实现。

1.3 The Three Levels of Software Development

  1. Level 1: Feature Delivery(功能交付)

    • 特点:快速响应需求,无严格设计;常见于初版或 PoC。
    • 弊端:代码结构混乱、难以测试、难以扩展。
  2. Level 2: Engineering Excellence(工程卓越)

    • 特点:引入流程(CI/CD、代码审查)、工具(静态分析、单元测试)。
    • 实践:使用 Git Hooks 强制代码风格;配置 CMake 脚本自动运行 Google Test。
  3. 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:子类覆盖时保证前置条件不加强、后置条件不削弱。
  • KISS 与 YAGNI:仅在需求明确时抽象新模块,避免过早引入复杂框架。

  • DRY:利用模板或宏消除重复,如对相似算法提炼为模板函数。

  • Separation of Concerns:UI、逻辑、存储分层;示例代码详见附录A。


Guideline 2: Design for Change

2.1 Separation of Concerns

  • 定义:将系统功能划分为相对独立的模块,每个模块专注单一职责。

  • 实践:使用命名空间、库分割和 CMake target 实现物理分层。

  • 示例:图像处理管道分为:

    1. 格式解析模块
    2. 像素变换模块
    3. 编码输出模块
      每一模块与其他仅通过纯函数或接口交互。

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++ 示例

    cpp 复制代码
    struct 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)
    • 对功能特征进行分层接口,避免单个模板参数承担过多职责。
  • 示例 :实现通用 serialize 模板,仅依赖 toString() 方法,而非整个对象。


Guideline 4: Design for Testability

4.1 How to Test a Private Member Function

  • 常见做法

    1. 使用 friend 声明测试类访问私有成员。
    2. 通过宏映射将私有方法编译为公共。
  • 弊端:破坏封装。

  • 示例

    cpp 复制代码
    class Foo { 
      private: int compute();
      friend class FooTest;
    };

4.2 The True Solution: Separate Concerns

  • 核心思路:将复杂逻辑从私有方法中抽离到独立类或函数,保证可直接测试。

  • 示例重构

    • 原始 Foo::process() 内部含 5 步算法:

      cpp 复制代码
      int a = step1(); int b = step2(a); ...
    • 重构后:

      cpp 复制代码
      class 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 动态加载新的实现
  • 示例

    cpp 复制代码
    struct IFormatter { virtual std::string format(int) = 0; };
    class JsonFormatter : public IFormatter {/*...*/};
    class XmlFormatter : public IFormatter {/*...*/};

5.2 Compile-Time Extensibility

  • 模板元编程 :利用模板特化、整型常量、if constexpr 实现编译期分支。

  • 参数化多态:CRTP、Traits 类。

  • 示例

    cpp 复制代码
    template<typename T> struct Serializer;
    template<> struct Serializer<Json> { static void serialize(...); };

5.3 Avoid Premature Design for Extension

  • 问题:过早设计插件机制或策略接口,若只有一个实现反而增加复杂度。

  • 建议

    • 根据 YAGNI 原则,仅在第二个实现需求出现时才引入扩展点。
    • 使用度量工具跟踪实现数量动态决策。

本章小结

  • 软件设计是一门结合科学严谨、工程流程与艺术创意的综合学科。
  • 五大指导原则(了解设计、应对变化、隔离接口、可测试性、可扩展性)协同作用。
  • 在 C++ 中,灵活运用模板、虚函数、多态、RAII 等特性,是实现高质量设计的关键。
  • 实战中,需根据项目规模与团队成熟度,平衡抽象深度与实现复杂度。
相关推荐
cch89181 小时前
汇编与Java:底层与高层的编程对决
java·开发语言·汇编
荒川之神2 小时前
拉链表概念与基本设计
java·开发语言·数据库
chushiyunen2 小时前
python中的@Property和@Setter
java·开发语言·python
小樱花的樱花3 小时前
C++ new和delete用法详解
linux·开发语言·c++
froginwe113 小时前
C 运算符
开发语言
fengfuyao9853 小时前
低数据极限下模型预测控制的非线性动力学的稀疏识别 MATLAB实现
开发语言·matlab
摇滚侠3 小时前
搭建前端开发环境 安装 nodejs 设置淘宝镜像 最简化最标准版本 不使用 NVM NVM 高版本无法安装低版本 nodejs
java·开发语言·node.js
t198751283 小时前
MATLAB十字路口车辆通行情况模拟系统
开发语言·matlab
yyk的萌4 小时前
AI 应用开发工程师基础学习计划
开发语言·python·学习·ai·lua
Amumu121385 小时前
Js:正则表达式(一)
开发语言·javascript·正则表达式