依赖倒置和里氏替换原则

1.依赖倒置原则

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

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

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

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

第二个条件的具体理解:

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

1.1 例子

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

// 具体的邮件发送类
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)依赖于这个抽象接口。两者都依赖于抽象。

// 定义抽象接口,代表消息发送者
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; // 依赖抽象接口
};

使用示例:

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的例子:

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;  // 确保正方形的宽高一致
    }
};

猜你喜欢

转载自blog.csdn.net/huanting74/article/details/143099304