依赖倒置和里氏替换原则

1.依赖倒置原则

依赖倒转原则(Dependency Inversion Principle,DIP)是面向对象设计中的五大设计原则之一。

它强调高层模块不应该依赖低层模块,两者都应该依赖于抽象 。即,使得系统的具体实现 依赖于抽象接口,而不是相反。

依赖倒转原则的核心思想:

  1. 高层模块 (调用者)不应该依赖于低层模块 (被调用者),二者都应该依赖于抽象
  2. 抽象 不应该依赖于细节细节 应该依赖于抽象

第二个条件的具体理解:

  • 抽象是指接口或抽象类,它们定义了系统的某种行为,而不关心其具体实现。依赖于抽象意味着,我们的高层模块(调用者)只关心执行任务的方式,而不关心具体的实现细节。
  • 细节 指的是具体的实现(例如发送邮件、发送短信的具体类)。这些具体的实现应该依赖于定义好的抽象接口。具体实现遵循抽象接口,满足系统的需求。

1.1 例子

违反依赖倒转原则的设计:

cpp 复制代码
// 具体的邮件发送类
class EmailSender {
public:
    void sendEmail(const std::string& message) {
        std::cout << "Sending Email: " << message << std::endl;
    }
};

// 高层模块,依赖于低层的 EmailSender
class NotificationService {
public:
    void send(const std::string& message) {
        emailSender.sendEmail(message); // 直接依赖具体类
    }
private:
    EmailSender emailSender; // 高层模块依赖于具体实现
};

这违反了依赖倒转原则,因为如果我们想要改变发送方式(如发送短信),就需要修改 NotificationService 的代码,耦合性高。

遵循依赖倒转原则的设计:

首先引入抽象层,让高层模块依赖抽象接口,而具体实现(如 EmailSenderSmsSender)依赖于这个抽象接口。两者都依赖于抽象。

cpp 复制代码
// 定义抽象接口,代表消息发送者
class IMessageSender {
public:
    virtual void sendMessage(const std::string& message) = 0;
    virtual ~IMessageSender() = default;
};

// 具体的邮件发送类,实现抽象接口
class EmailSender : public IMessageSender {
public:
    void sendMessage(const std::string& message) override {
        std::cout << "Sending Email: " << message << std::endl;
    }
};

// 具体的短信发送类,实现抽象接口
class SmsSender : public IMessageSender {
public:
    void sendMessage(const std::string& message) override {
        std::cout << "Sending SMS: " << message << std::endl;
    }
};

// 高层模块依赖于抽象接口 IMessageSender,而不是具体实现
class NotificationService {
public:
    NotificationService(IMessageSender* sender) : messageSender(sender) {}
    
    void send(const std::string& message) {
        messageSender->sendMessage(message); // 通过接口调用,不依赖具体实现
    }
private:
    IMessageSender* messageSender; // 依赖抽象接口
};

使用示例:

cpp 复制代码
int main() {
    EmailSender emailSender;
    NotificationService notificationService(&emailSender);
    notificationService.send("Hello via Email!");

    SmsSender smsSender;
    NotificationService smsNotificationService(&smsSender);
    smsNotificationService.send("Hello via SMS!");

    return 0;
}

这种设计,我们遵循了依赖倒转 原则,使得高层模块不再直接依赖于低层的具体实现,而是依赖抽象接口。这使得系统的扩展性和维护性更好。

1.2 依赖关系传递的三种方式

接口传递,构造方法传递,setter 方式传递

依赖倒转原则的注意事项和细节

  • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好

  • 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化

  • 继承时遵循里氏替换原则

2.里氏替换

里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一项重要原则。 其核心思想是子类必须能够替换父类,并且不会影响程序的正确性

2.1 里氏替换原则的好处

  • 提高代码的可扩展性和复用性:遵循LSP,子类可以无缝替代父类。这意味着系统可以在不修改原有代码的基础上,通过新增子类实现扩展功能,从而使代码更具扩展性。

  • 增强系统的灵活性:使用父类类型来定义变量、参数或返回值,可以通过子类的多态特性来动态改变行为,实现更灵活的设计。

  • 确保继承关系的合理性 :LSP帮助避免不合适的继承。如果子类不能完全替代父类,说明这个子类与父类的关系可能存在问题。这样的继承会导致代码结构复杂化,破坏代码的可维护性。

违反LSP的例子:

cpp 复制代码
class Rectangle {
public:
    virtual void setWidth(double width) { this->width = width; }
    virtual void setHeight(double height) { this->height = height; }
    double getArea() const { return width * height; }

protected:
    double width;
    double height;
};

class Square : public Rectangle {
public:
    void setWidth(double width) override {
        this->width = width;
        this->height = width;  // 确保正方形的宽高一致
    }

    void setHeight(double height) override {
        this->width = height;
        this->height = height;  // 确保正方形的宽高一致
    }
};
相关推荐
不是仙人的闲人2 个月前
面向对象程序设计原则——里氏替换原则(LSP)
c++·设计模式·里氏替换原则
J老熊2 个月前
设计模式六大原则:里氏替换原则详细说明和案例示范
java·设计模式·面试·系统架构·里氏替换原则
shiming88793 个月前
设计模式六大原则之里氏替换原则(Liskov Substitution Principle, LSP)
设计模式·里氏替换原则
hong1616883 个月前
设计模式六大原则中的里氏替换原则
java·设计模式·里氏替换原则
2401_858120263 个月前
Swift语言服务器协议(LSP)深度解析:开启Swift开发的新篇章
服务器·swift·里氏替换原则
贺仙姑3 个月前
里氏替换原则(LSP)
java·开发语言·里氏替换原则
A22744 个月前
零——七大设计原则
接口隔离原则·依赖倒置原则·里氏替换原则·开闭原则·迪米特法则·合成复用原则·单一职责原则
codefly-xtl4 个月前
里氏替换原则
里氏替换原则
洋柿子08264 个月前
整洁架构SOLID-里氏替换原则(LSP)
架构·里氏替换原则