C++ SOLID 原则学习笔记

1. SRP:单一职责原则 (Single Responsibility Principle)

原则:一个类应该只有一个引起它变化的原因(即只做一件事)。

反例 上帝类 (God Class)

一个 VM 类既负责执行指令,又负责文件 IO,还负责格式化输出日志。

cpp 复制代码
class VM {
public:
    void Run(const char* bytecode);  // 核心业务
    void LoadFile(const std::string& path); // IO 职责
    void LogError(const std::string& msg);  // 日志/UI 职责
};

问题:如果我想改日志格式,得改 VM;如果我想改文件读取方式,也得改 VM。VM 类会变得极其臃肿。

正例 职责分离

将辅助功能剥离,VM 只关注核心的"执行"。

cpp 复制代码
// 1. Loader 负责加载
class ProgramLoader {
public:
    std::vector<uint8_t> Load(const std::string& path);
};

// 2. Logger 负责日志
class Logger {
public:
    void Log(const std::string& msg);
};

// 3. VM 只负责执行
class VM {
public:
    void Run(const std::vector<uint8_t>& code);
};

实战思考 :在 cilly-vm-cpp 中,StackStats 就是一个很好的 SRP 实践------它把栈统计逻辑从 VM 中剥离了出来。


2. OCP:开闭原则 (Open/Closed Principle)

原则 :软件实体(类、模块、函数)应该对扩展开放,对修改关闭 。 核心 :加新功能时,最好只加新代码 ,而不是改旧代码。

反例 Switch 满天飞

在 AST 处理中,使用 enum 和 switch 是最容易违反 OCP 的地方。

cpp 复制代码
enum class ExprKind { kAdd, kSub };

void Evaluate(Expr* expr) {
    switch (expr->kind) {
        case kAdd: /* ... */ break;
        case kSub: /* ... */ break;
        // 每次加新类型 kMul,都要来这里改代码!
    }
}

正例 访问者模式 (Visitor Pattern)

利用多态和双分派,将操作抽象为 Visitor。

cpp 复制代码
// 1. 定义访问者接口
class ExprVisitor {
public:
    virtual void Visit(class AddExpr* expr) = 0;
    virtual void Visit(class SubExpr* expr) = 0;
};

// 2. 元素基类接受访问者
struct Expr {
    virtual void Accept(ExprVisitor& v) = 0;
};

// 3. 具体元素实现 Accept
struct AddExpr : Expr {
    void Accept(ExprVisitor& v) override { v.Visit(this); }
};

// 4. 扩展功能:只需新增 Visitor,无需改动 Expr 类
class Printer : public ExprVisitor {
    void Visit(AddExpr* expr) override { std::cout << "+"; }
    void Visit(SubExpr* expr) override { std::cout << "-"; }
};

3. LSP:里氏替换原则 (Liskov Substitution Principle)

原则 :子类对象必须能够替换掉所有父类对象,而不会导致程序错误。 通俗理解 :父类能用的地方,换成子类也能用,且不会炸。

反例 正方形不是长方形

cpp 复制代码
class Rectangle {
public:
    virtual void SetWidth(int w) { width_ = w; }
    virtual void SetHeight(int h) { height_ = h; }
    int Area() const { return width_ * height_; }
protected:
    int width_, height_;
};

class Square : public Rectangle {
public:
    // 破坏了父类的语义:设置宽会导致高也变化
    void SetWidth(int w) override { width_ = height_ = w; }
    void SetHeight(int h) override { width_ = height_ = h; }
};

void Resize(Rectangle& r) {
    r.SetWidth(5);
    r.SetHeight(4);
    assert(r.Area() == 20); // 传入 Square 时,这里会断言失败(结果是 16)
}

正例 GC 对象契约

在 GC 系统中,所有对象都继承自 GcObject。

cpp 复制代码
class GcObject {
public:
    // 契约:子类必须在这个函数里报告它引用的所有子对象
    virtual void Trace(Collector& c) = 0;
};

// 只要 StringObject 正确实现了 Trace(哪怕它是空的),
// 它就能被 Collector 正确处理,不会导致 GC 崩溃。
class StringObject : public GcObject {
    void Trace(Collector& c) override { /* 无引用,空实现即可 */ }
};

4. ISP:接口隔离原则 (Interface Segregation Principle)

原则 :客户端不应该依赖它不需要的接口。 核心 :接口要小而专,避免大而全。

反例 胖接口

cpp 复制代码
class ISmartObject {
public:
    virtual void Serialize() = 0; // 序列化
    virtual void Trace() = 0;     // GC 标记
    virtual void Draw() = 0;      // UI 绘制
};

// 仅仅是一个简单的数据对象,却被迫实现 Draw()
class DataNode : public ISmartObject {
    void Serialize() override { ... }
    void Trace() override { ... }
    void Draw() override { /* 抛异常或空实现 */ } // 违反 ISP
};

正例 接口拆分

cpp 复制代码
class ISerializable { virtual void Serialize() = 0; };
class IGcTraceable { virtual void Trace() = 0; };
class IDrawable { virtual void Draw() = 0; };

// 按需组合
class DataNode : public ISerializable, public IGcTraceable { ... };
class UiNode : public IDrawable, public IGcTraceable { ... };

5. DIP:依赖倒置原则 (Dependency Inversion Principle)

原则 :高层模块不应该依赖低层模块,二者都应该依赖其抽象。 核心 :面向接口编程。

反例 依赖具体实现

VM 直接依赖具体的标记清除 GC (MarkSweepCollector)。

cpp 复制代码
class MarkSweepCollector { ... };

class VM {
    MarkSweepCollector gc_; // 强耦合!
public:
    VM() { ... }
};

如果我想换成引用计数 GC (RefCountCollector),必须修改 VM 类的定义。

正例 依赖抽象接口

cpp 复制代码
// 1. 定义抽象接口
class IGcCollector {
public:
    virtual void Collect() = 0;
    virtual ~IGcCollector() = default;
};

// 2. VM 依赖接口
class VM {
    IGcCollector* gc_; 
public:
    // 3. 通过构造函数注入具体实现
    VM(IGcCollector* gc) : gc_(gc) {}
};

// 4. 外部决定用哪种 GC
int main() {
    MarkSweepCollector my_gc;
    VM vm(&my_gc); 
    // 或者 VM vm(new CopyingCollector());
}

总结

  • SRP: 一个类只做一件事。
  • OCP: 多态是关键,加新功能不改旧代码。
  • LSP: 子类必须能完全替代父类(行为不缩水、不捣乱)。
  • ISP: 接口要小,不要强迫子类实现没用的方法。
  • DIP: 依赖抽象接口,方便替换底层实现。
相关推荐
白杨尚青5 小时前
C++进阶篇(二):多态——虚函数、重写、虚函数表与三个经典坑(附完整代码仓库)
开发语言·c++·多态·虚函数·重写·动态绑定
syagain_zsx6 小时前
spdlog实战:日志分级、滚动与性能调优
linux·c++·日志库·二次开发日志库
weixin_307779131 天前
C++代码实现MATLAB中的fitrtree函数功能
开发语言·c++·算法·matlab
AI wins1 天前
c++新增頭文件
c++
Shadow(⊙o⊙)1 天前
仿muduo实现高并发服务器——事件就绪管理Channel.hpp
服务器·c++
by209991 天前
哈希表——快速找到数据:冲突、删除与扩容的实现
数据结构·c++·哈希算法·散列表
音草1 天前
C++ 继承详解:从概念到菱形继承
c++
weixin_307779131 天前
C++代码实现MATLAB中的spectralcluster函数功能
开发语言·c++·算法·matlab
xiaoye-duck1 天前
《C++ 项目:手写大模型接入 SDK》Trae IDE 开发环境搭建与依赖管理
c++·ide·语言模型
沫璃染墨1 天前
《Qt从零入门系列(十四):QThread多线程详解——从线程创建到客户端阻塞处理》
开发语言·c++·笔记·qt·ui·交互·线程