1.依赖倒置原则
依赖倒转原则(Dependency Inversion Principle,DIP)是面向对象设计中的五大设计原则之一。
它强调高层模块不应该依赖低层模块,两者都应该依赖于抽象 。即,使得系统的具体实现 依赖于抽象接口,而不是相反。
依赖倒转原则的核心思想:
- 高层模块 (调用者)不应该依赖于低层模块 (被调用者),二者都应该依赖于抽象。
- 抽象 不应该依赖于细节 ,细节 应该依赖于抽象。
第二个条件的具体理解:
- 抽象是指接口或抽象类,它们定义了系统的某种行为,而不关心其具体实现。依赖于抽象意味着,我们的高层模块(调用者)只关心执行任务的方式,而不关心具体的实现细节。
- 细节 指的是具体的实现(例如发送邮件、发送短信的具体类)。这些具体的实现应该依赖于定义好的抽象接口。具体实现遵循抽象接口,满足系统的需求。
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
的代码,耦合性高。
遵循依赖倒转原则的设计:
首先引入抽象层,让高层模块依赖抽象接口,而具体实现(如 EmailSender
或 SmsSender
)依赖于这个抽象接口。两者都依赖于抽象。
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; // 确保正方形的宽高一致
}
};