目录
背景
大家都知道C++是面向对象开发的,封装、继承与多态是面向对象开发的三大特征。相信大家都听说过设计模式,但SOLID设计原则不知道大家有没有了解过,这个问题面试也经常会问。
SOLID原则是五个面向对象设计原则的首字母缩写,它们旨在提高软件的可维护性和可扩展性。这些原则是由罗伯特·C·马丁(Robert C. Martin)在20世纪90年代提出的,分别是单一职责原则(S)、开放封闭原则(O)、里氏替换原则(L)、接口隔离原则(I)、依赖倒置原则(D)。
单一职责原则
单一职责原则(SRP - Single Responsibility Principle):一个类只负责一件事,应该只有一个引起它变化的原因。
例子:一个类应该只负责一个功能。
// 违反单一职责原则的类
class Logger {
public:
void log(const std::string& message) {
// 保存日志到文件
}
void sendAlert(const std::string& message) {
// 发送警告邮件
}
};
// 遵守单一职责原则的类
class FileLogger {
public:
void log(const std::string& message) {
// 保存日志到文件
}
};
class AlertLogger {
public:
void sendAlert(const std::string& message) {
// 发送警告邮件
}
};
开放封闭原则
开放封闭原则(OCP - Open/Closed Principle):类应该通过扩展而非修改来应对需求变化。软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
例子:通过继承和多态实现扩展,而不是修改现有代码。
class Shape {
public:
virtual double area() const = 0;
};
class Circle : public Shape {
public:
double area() const override {
return 3.14 * 4 * 4; // 假设半径为2
}
};
class Square : public Shape {
public:
double area() const override {
return 4 * 4; // 边长为4
}
};
// 新增形状时,只需添加新的类,无需修改现有代码
class Triangle : public Shape {
public:
double area() const override {
return 0.5 * 4 * 4; // 底和高都是4
}
};
里氏替换原则
里氏替换原则(LSP - Liskov Substitution Principle):子类可以替代父类,不改变程序行为以及程序的正确性。
错误示例:
#include <iostream>
// 基类
class Rectangle {
public:
virtual void setWidth(int width) {
this->width = width;
}
virtual void setHeight(int height) {
this->height = height;
}
virtual int getArea() const {
return width * height;
}
protected:
int width;
int height;
};
// 派生类
class Square : public Rectangle {
public:
void setWidth(int width) override {
this->width = height = width; // 保持宽高相等
}
void setHeight(int height) override {
this->height = width = height; // 保持宽高相等
}
};
void processShape(Rectangle& shape) {
shape.setWidth(5);
shape.setHeight(10);
std::cout << "Expected area: 50, Got area: " << shape.getArea() << std::endl;
}
int main() {
Rectangle rect;
Square sqr;
processShape(rect); // 处理矩形
processShape(sqr); // 处理正方形
return 0;
}
在这个例子中,Square 类是从 Rectangle 类派生的。根据LSP,任何父类 (Rectangle) 的对象都可以被其子类 (Square) 的对象替换,而不改变程序的预期行为。在 processShape 函数中,我们期望无论传入的是 Rectangle 还是 Square 对象,理论应该都能正确计算面积。
然而,由于 Square 类重写了 setWidth 和 setHeight 方法,使得设置宽度和高度时会相互影响,这违反了LSP原则。因此,尽管 Square 是 Rectangle 的子类型,但它们的行为并不完全一致,导致在使用 Square 对象时,程序的行为与预期不符。
正确的示例应该创建一个更通用的 Shape
基类,让 Rectangle
和 Square
都继承自这个基类去遵循里氏替换原则。
接口隔离原则
接口隔离原则(ISP - Interface Segregation Principle):不应强制客户端依赖于它们不使用的接口,接口应该小而精,避免无关功能。
这个原则很好理解,简单解释一下,现在有两个客户user1和user2,对于user1而言,他会用到所有OPS的接口,对于user2,这个客户只需要用OPS里面的op1和op2这两个接口,别的都用不到,这时候开发人员只需要在中间加一个IUOps隔离开然后交付给user2就可以了。
错误示例:
// 不合理的接口,违反了接口隔离原则
class IAnimal {
public:
virtual void eat() = 0;
virtual void fly() = 0;
};
// 鸟类实现了吃和飞
class Bird : public IAnimal {
public:
void eat() override {
std::cout << "Bird is eating." << std::endl;
}
void fly() override {
std::cout << "Bird is flying." << std::endl;
}
};
// 鱼类只实现了吃,没有实现飞
class Fish : public IAnimal {
public:
void eat() override {
std::cout << "Fish is eating." << std::endl;
}
void fly() override {
// 鱼类不需要飞,这里可以抛出异常或者不实现该方法
std::cout << "Fish cannot fly." << std::endl;
}
};
以上代码由于Fish类不需要fly方法,我们可以认为IAnimal接口违反了接口隔离原则。为了解决这个问题,我们可以将IAnimal接口拆分成两个更小的接口:
正确代码:
// 将IAnimal拆分成两个接口
class IEat {
public:
virtual void eat() = 0;
};
class IFly {
public:
virtual void fly() = 0;
};
// 鸟类实现了吃和飞
class Bird : public IEat, public IFly {
public:
void eat() override {
std::cout << "Bird is eating." << std::endl;
}
void fly() override {
std::cout << "Bird is flying." << std::endl;
}
};
// 鱼类只实现了吃
class Fish : public IEat {
public:
void eat() override {
std::cout << "Fish is eating." << std::endl;
}
};
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
-
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
-
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
依赖倒置原则
依赖倒置原则(DIP - Dependency Inversion Principle):高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
例子:通过抽象(接口或抽象类)来实现模块间的依赖关系。
#include <iostream>
// 定义一个抽象的日志记录器接口
class ILogger {
public:
virtual ~ILogger() {}
virtual void Log(const std::string& message) = 0;
};
// 实现具体的控制台日志记录器
class ConsoleLogger : public ILogger {
public:
void Log(const std::string& message) override {
std::cout << "Console Logger: " << message << std::endl;
}
};
// 实现具体的文件日志记录器
class FileLogger : public ILogger {
public:
void Log(const std::string& message) override {
// 假设这里写入到文件,实际中需要打开文件等操作
std::cout << "File Logger: " << message << std::endl;
}
};
// 应用程序类,依赖于日志记录器的接口而不是具体实现
class Application {
private:
ILogger* logger; // 使用接口指针来引用日志记录器
public:
Application(ILogger* logger) : logger(logger) {}
void Run() {
// 业务逻辑...
logger->Log("Application is running.");
}
};
int main() {
// 创建具体的日志记录器实例
ConsoleLogger consoleLogger;
FileLogger fileLogger;
// 将具体的日志记录器注入到应用程序中
Application app1(&consoleLogger);
app1.Run();
Application app2(&fileLogger);
app2.Run();
return 0;
}
在这个示例中,ILogger
是一个抽象的日志记录器接口,它定义了一个Log
方法。ConsoleLogger
和FileLogger
是这个接口的两个具体实现,分别用于在控制台和文件中记录日志。Application
类依赖于ILogger
接口,而不是具体的实现,这符合依赖倒置原则。通过这种方式,我们可以很容易地替换或扩展日志记录方式,而无需修改Application
类的代码。
总结
1.单一职责原则(SRP - Single Responsibility Principle):一个类只负责一件事。
2.开放封闭原则(OCP - Open/Closed Principle):类应该通过扩展而非修改来应对需求变化。
3.里氏替换原则(LSP - Liskov Substitution Principle):子类可以替代父类,不改变程序行为。
4.接口隔离原则(ISP - Interface Segregation Principle):不应强制客户端依赖于它们不使用的接口,接口应该小而精,避免无关功能。
5.依赖倒置原则(DIP - Dependency Inversion Principle):高层模块依赖于抽象而非具体实现。
最后,如果觉得我的文章对你有帮助,请点赞收藏加关注,谢谢大家。