目录
[一、里氏替换原则(Liskov Substitution Principle)](#一、里氏替换原则(Liskov Substitution Principle))
[二、接口隔离原则(Interface Segregation Principle)](#二、接口隔离原则(Interface Segregation Principle))
[三、依赖倒置原则(Dependency Inversion Principle)](#三、依赖倒置原则(Dependency Inversion Principle))
[五、SOLID 原则总结](#五、SOLID 原则总结)
一、里氏替换原则(Liskov Substitution Principle)
子类对象必须能够替换父类对象,而不影响程序的正确性。
换句话说:任何使用基类的地方,都可以透明地使用派生类。如果替换后程序行为异常,说明继承关系设计有问题。
违反原则的例子:企鹅不是鸟?
cpp
// ❌ 违反里氏替换
class Bird {
public:
virtual void fly() {
cout << "鸟儿飞翔" << endl;
}
virtual ~Bird() = default;
};
class Penguin : public Bird {
public:
void fly() override {
// 企鹅不会飞!这里只能抛异常或空实现
throw logic_error("企鹅不会飞");
}
};
void makeBirdFly(Bird& b) {
b.fly(); // 传入 Penguin 时崩溃
}
这里 Penguin 无法替换 Bird ------ 因为不是所有鸟都会飞。
正确的设计:分离接口
cpp
// ✅ 符合里氏替换
class Bird {
// 公共特征
};
class Flyable {
public:
virtual void fly() = 0;
virtual ~Flyable() = default;
};
class Sparrow : public Bird, public Flyable {
public:
void fly() override {
cout << "麻雀飞翔" << endl;
}
};
class Penguin : public Bird {
// 企鹅没有 Flyable 接口
};
void makeItFly(Flyable& f) {
f.fly(); // 只接受会飞的东西
}
里氏替换的常见违反模式
| 违反模式 | 例子 | 正确做法 |
|---|---|---|
| 派生类抛出基类没有的异常 | Penguin::fly() 抛异常 |
重新设计继承体系 |
| 派生类改变基类的语义 | Square 继承 Rectangle,但修改宽度时高度不变 |
用组合代替继承 |
| 派生类删除了基类的功能 | override 后空实现或 assert(false) |
接口分离 |
二、接口隔离原则(Interface Segregation Principle)
不应该强迫客户端依赖它们不使用的方法。
大而全的接口会导致"胖接口"------实现类被迫实现一些它不需要的方法。
违反原则的例子:全能工作台
cpp
// ❌ 违反接口隔离:胖接口
class Worker {
public:
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
virtual void code() = 0;
virtual void design() = 0;
virtual void test() = 0;
virtual ~Worker() = default;
};
// 机器人不需要 eat/sleep,但被迫实现
class Robot : public Worker {
public:
void work() override { cout << "机器人工作" << endl; }
void eat() override { /* 空实现,什么都不做 */ }
void sleep() override { /* 空实现 */ }
void code() override { cout << "机器人编码" << endl; }
void design() override { /* 空实现 */ }
void test() override { cout << "机器人测试" << endl; }
};
重构:拆分接口
cpp
// ✅ 符合接口隔离:多个小接口
class Workable {
public:
virtual void work() = 0;
virtual ~Workable() = default;
};
class Eatable {
public:
virtual void eat() = 0;
virtual ~Eatable() = default;
};
class Sleepable {
public:
virtual void sleep() = 0;
virtual ~Sleepable() = default;
};
class Codable {
public:
virtual void code() = 0;
virtual ~Codable() = default;
};
// 机器人只实现它需要的接口
class Robot : public Workable, public Codable, public Testable {
public:
void work() override { cout << "机器人工作" << endl; }
void code() override { cout << "机器人编码" << endl; }
void test() override { cout << "机器人测试" << endl; }
};
// 人类可以实现所有接口
class Human : public Workable, public Eatable, public Sleepable,
public Codable, public Designable, public Testable {
// 全部实现
};
接口隔离的收益
| 问题 | 违反时 | 符合时 |
|---|---|---|
| 实现类 | 被迫实现空方法 | 只实现需要的接口 |
| 修改接口 | 影响所有实现类 | 只影响相关实现类 |
| 客户端 | 依赖不需要的方法 | 只依赖需要的方法 |
三、依赖倒置原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
通俗地说:要依赖接口/抽象类,不要依赖具体类。
违反原则的例子
cpp
// ❌ 违反依赖倒置:高层依赖低层
class EmailSender {
public:
void send(const string& msg) {
cout << "发送邮件: " << msg << endl;
}
};
class NotificationService {
EmailSender sender; // 直接依赖具体类
public:
void notify(const string& msg) {
sender.send(msg);
}
};
问题:如果想换成短信或微信通知,必须修改 NotificationService。
重构:依赖抽象
cpp
// ✅ 符合依赖倒置
class IMessageSender {
public:
virtual void send(const string& msg) = 0;
virtual ~IMessageSender() = default;
};
class EmailSender : public IMessageSender {
public:
void send(const string& msg) override {
cout << "发送邮件: " << msg << endl;
}
};
class SmsSender : public IMessageSender {
public:
void send(const string& msg) override {
cout << "发送短信: " << msg << endl;
}
};
class NotificationService {
IMessageSender& sender; // 依赖抽象
public:
NotificationService(IMessageSender& s) : sender(s) {}
void notify(const string& msg) {
sender.send(msg);
}
};
// 使用
int main() {
EmailSender email;
NotificationService service(email);
service.notify("Hello");
SmsSender sms;
NotificationService service2(sms); // 同样可以工作
}
依赖注入
上面的例子使用了构造函数注入------依赖通过构造函数传入。这是实现依赖倒置的常用模式。
四、完整例子:从违反到符合
假设有一个订单处理系统,初始版本违反多个原则。
版本1:违反所有原则
cpp
// ❌ 违反:依赖倒置、接口隔离、里氏替换
class MySQLDatabase {
public:
void save(const string& data) {
cout << "保存到 MySQL: " << data << endl;
}
};
class PDFExporter {
public:
void exportToPDF(const string& data) {
cout << "导出 PDF: " << data << endl;
}
};
// 这个类做了太多事,依赖具体类
class OrderProcessor {
MySQLDatabase db;
PDFExporter exporter;
public:
void process(const string& order) {
// 处理订单
string result = "Processed: " + order;
db.save(result);
exporter.exportToPDF(result);
}
};
版本2:重构后
cpp
// ========== 1. 数据存储接口(依赖倒置)==========
class IDataStorage {
public:
virtual void save(const string& data) = 0;
virtual ~IDataStorage() = default;
};
class MySQLStorage : public IDataStorage {
public:
void save(const string& data) override {
cout << "保存到 MySQL: " << data << endl;
}
};
class RedisStorage : public IDataStorage {
public:
void save(const string& data) override {
cout << "保存到 Redis: " << data << endl;
}
};
// ========== 2. 报表导出接口(接口隔离)==========
class IPDFExportable {
public:
virtual void exportToPDF(const string& data) = 0;
virtual ~IPDFExportable() = default;
};
class PDFExporter : public IPDFExportable {
public:
void exportToPDF(const string& data) override {
cout << "导出 PDF: " << data << endl;
}
};
// ========== 3. 订单处理器(依赖抽象)==========
class OrderProcessor {
IDataStorage& storage;
IPDFExportable& exporter;
public:
OrderProcessor(IDataStorage& s, IPDFExportable& e)
: storage(s), exporter(e) {}
void process(const string& order) {
string result = "Processed: " + order;
storage.save(result);
exporter.exportToPDF(result);
}
};
// ========== 4. 扩展:新功能只需加新类 ==========
// 新增 Excel 导出(符合开闭原则)
class IExcelExportable {
public:
virtual void exportToExcel(const string& data) = 0;
virtual ~IExcelExportable() = default;
};
class ExcelExporter : public IExcelExportable {
public:
void exportToExcel(const string& data) override {
cout << "导出 Excel: " << data << endl;
}
};
// 如果需要同时支持 Excel,可以扩展 OrderProcessor
// 或者创建一个新的 EnhancedOrderProcessor,不修改原有类
int main() {
MySQLStorage mysql;
PDFExporter pdf;
OrderProcessor processor(mysql, pdf);
processor.process("订单#12345");
RedisStorage redis;
OrderProcessor processor2(redis, pdf); // 换数据库,无需改代码
processor2.process("订单#67890");
return 0;
}
五、SOLID 原则总结
| 原则 | 一句话 | 关键点 |
|---|---|---|
| 单一职责 | 一个类只做一件事 | 类只有一个变化的原因 |
| 开闭原则 | 对扩展开放,对修改关闭 | 多态、抽象基类 |
| 里氏替换 | 子类必须能替换父类 | 继承要符合 is-a 语义 |
| 接口隔离 | 接口要小而专一 | 拆分胖接口 |
| 依赖倒置 | 依赖抽象而非具体 | 依赖注入、面向接口编程 |
六、常见误区
误区1:为了原则而过度设计
一个只有 3 个类的项目不需要应用所有 SOLID 原则。原则用于管理复杂性,不要在简单项目中过度工程。
误区2:认为里氏替换禁止任何行为改变
里氏替换不要求派生类的行为与基类完全相同,只要求不违反基类的契约(前置条件不加强,后置条件不削弱)。
误区3:依赖倒置意味着完全不使用具体类
依赖倒置是指高层模块 依赖抽象,低层模块可以是具体类。程序总要有地方 new 具体对象(通常放在 main 或工厂中)。
七、这一篇的收获
你现在应该理解:
-
里氏替换:子类必须能安全地替换父类;会飞的鸟和不会飞的鸟应该分开建模
-
接口隔离:胖接口要拆分成多个小接口;客户端只依赖它需要的方法
-
依赖倒置:依赖抽象(接口/抽象类),不依赖具体实现;通过依赖注入实现
-
SOLID 整体 :五原则相互配合,目标是低耦合、高内聚、易扩展
💡 小作业:找出你项目中的一个"胖接口"类(或者写一个示例),按照接口隔离原则拆分成至少 3 个小接口。然后修改客户端代码,让它们只依赖自己需要的接口。
下一篇预告 :第39篇《简单工厂模式与工厂方法模式:C++实现》------进入设计模式实战。工厂模式封装对象创建逻辑,让客户端不直接 new。简单工厂和工厂方法有什么区别?C++ 中如何实现?下篇详解。