疯狂餐厅
86.88M · 2026-03-21
一个 VM 类既负责执行指令,又负责文件 IO,还负责格式化输出日志。
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 只关注核心的“执行”。
// 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 中剥离了出来。
在 AST 处理中,使用 enum 和 switch 是最容易违反 OCP 的地方。
enum class ExprKind { kAdd, kSub };
void Evaluate(Expr* expr) {
switch (expr->kind) {
case kAdd: /* ... */ break;
case kSub: /* ... */ break;
// 每次加新类型 kMul,都要来这里改代码!
}
}
利用多态和双分派,将操作抽象为 Visitor。
// 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 << "-"; }
};
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 系统中,所有对象都继承自 GcObject。
class GcObject {
public:
// 契约:子类必须在这个函数里报告它引用的所有子对象
virtual void Trace(Collector& c) = 0;
};
// 只要 StringObject 正确实现了 Trace(哪怕它是空的),
// 它就能被 Collector 正确处理,不会导致 GC 崩溃。
class StringObject : public GcObject {
void Trace(Collector& c) override { /* 无引用,空实现即可 */ }
};
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
};
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 { ... };
VM 直接依赖具体的标记清除 GC (MarkSweepCollector)。
class MarkSweepCollector { ... };
class VM {
MarkSweepCollector gc_; // 强耦合!
public:
VM() { ... }
};
如果我想换成引用计数 GC (RefCountCollector),必须修改 VM 类的定义。
// 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());
}