【c++面向对象编程】第38篇:设计原则(二):里氏替换、接口隔离与依赖倒置

目录

[一、里氏替换原则(Liskov Substitution Principle)](#一、里氏替换原则(Liskov Substitution Principle))

违反原则的例子:企鹅不是鸟?

正确的设计:分离接口

里氏替换的常见违反模式

[二、接口隔离原则(Interface Segregation Principle)](#二、接口隔离原则(Interface Segregation Principle))

违反原则的例子:全能工作台

重构:拆分接口

接口隔离的收益

[三、依赖倒置原则(Dependency Inversion Principle)](#三、依赖倒置原则(Dependency Inversion Principle))

违反原则的例子

重构:依赖抽象

依赖注入

四、完整例子:从违反到符合

版本1:违反所有原则

版本2:重构后

[五、SOLID 原则总结](#五、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++ 中如何实现?下篇详解。

相关推荐
猫猫的小茶馆4 小时前
【Python】函数与模块化编程
linux·开发语言·arm开发·驱动开发·python·stm32
_院长大人_4 小时前
Java Excel导出:如何实现自定义表头与字段顺序的完全控制
java·开发语言·后端·excel
code_whiter4 小时前
C++1进阶(继承)
开发语言·c++
来恩10034 小时前
JSTL的标签库种类
java·开发语言
Miss_min4 小时前
128K长序列数据生成
开发语言·python·深度学习
小宋0014 小时前
QT中控件qss样式修改
开发语言·qt
图像僧4 小时前
vs2019中的属性页使用说明
java·开发语言·jvm
YOU OU5 小时前
SpringBoot 日志
java·开发语言
智者知已应修善业5 小时前
【51单片机LED闪烁10次数码管显示0-9】2023-12-14
c++·经验分享·笔记·算法·51单片机