【c++面向对象编程】第38篇:设计原则(二):里氏替换、接口隔离与依赖倒置
目录一、里氏替换原则Liskov Substitution Principle违反原则的例子企鹅不是鸟正确的设计分离接口里氏替换的常见违反模式二、接口隔离原则Interface Segregation Principle违反原则的例子全能工作台重构拆分接口接口隔离的收益三、依赖倒置原则Dependency Inversion Principle违反原则的例子重构依赖抽象依赖注入四、完整例子从违反到符合版本1违反所有原则版本2重构后五、SOLID 原则总结六、常见误区误区1为了原则而过度设计误区2认为里氏替换禁止任何行为改变误区3依赖倒置意味着完全不使用具体类七、这一篇的收获一、里氏替换原则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 中如何实现下篇详解。