一、单一职责原则(SRP)
1、概念
单一职责原则是一种编程原则,要求一个类应该只有一个引发其变化的原因。换句话说,一个类应该只负责一项职责。
2、为什么重要
- 可维护性:遵循单一职责原则的代码更容易维护,因为每个类都有明确的职责。
- 可读性:代码结构更清晰,每个类的功能更易于理解。
- 可扩展性:当需要添加新功能或修改现有功能时,遵循这一原则可以减少对其他部分代码产生影响的可能性。
3、如何实现
- 在设计类的时候,仔细考虑其职责,并尽量确保每个类只做一件事。
- 如果发现一个类有多重职责,考虑将其分解成多个只有单一职责的类。
4、C++代码示例
下面的代码展示一个违反单一职责原则的例子和其修正版本。
违反SRP原则的代码
#include <iostream>
#include <string>
#include <fstream>
class Report {
public:
Report(const std::string& title) : title(title) {}
void generate() {
std::cout << "Generating report with title: " << title << std::endl;
// ...其他逻辑
saveToFile();
}
void saveToFile() {
std::ofstream file("report.txt");
if (file.is_open()) {
file << "Report title: " << title << std::endl;
// ...其他内容
file.close();
}
}
private:
std::string title;
};
int main() {
Report report("Monthly Sales");
report.generate();
return 0;
}
代码中,Report
类有两个不同的职责:
-
报告生成(Generating Report):该类负责生成报告,这通常涉及到格式化数据、应用业务逻辑等。
-
文件保存(Saving to File):该类还负责将生成的报告保存到磁盘上的一个文件中。
这两个职责应当是分开的,下面是正确的写法,Report
类只负责报告的生成,而ReportSaver
类负责将报告保存到文件。这样,每个类都只有一个职责,遵循了单一职责原则。
#include <iostream>
#include <string>
#include <fstream>
class Report {
public:
Report(const std::string& title) : title(title) {}
void generate() {
std::cout << "Generating report with title: " << title << std::endl;
// ...其他逻辑
}
private:
std::string title;
};
class ReportSaver {
public:
static void saveToFile(const Report& report) {
std::ofstream file("report.txt");
if (file.is_open()) {
file << "Report content..." << std::endl;
// ...其他内容
file.close();
}
}
};
int main() {
Report report("Monthly Sales");
report.generate();
ReportSaver::saveToFile(report);
return 0;
}
二、开放封闭原则( OCP)
1、原则概述
开放封闭原则是面向对象设计原则之一,它指出软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着你的代码应该允许在不修改现有代码的情况下添加新功能。
2、结构
-
定义:
- 对扩展开放:新的功能应该通过添加新代码来实现。
- 对修改封闭:一旦一个模块的行为被确定,应该避免修改它。
-
目的:
- 提高代码的可维护性。
- 提高代码的可复用性。
- 增加代码的健壮性。
-
应用场景:
- 需要添加新功能或变更需求时。
- 多人或多团队并行开发时,为减少彼此之间的依赖。
-
实现方式:
- 使用抽象类和接口。
- 使用装饰器模式、策略模式等设计模式。
3、代码示例
假设有一个绘图程序,需要绘制不同形状。如果不使用开放封闭原则,代码如下:
enum ShapeType { Circle, Square };
class Shape {
public:
ShapeType type;
};
class Circle : public Shape {
public:
Circle() { type = Circle; }
};
class Square : public Shape {
public:
Square() { type = Square; }
};
void drawShape(Shape* shape) {
if (shape->type == Circle) {
// 绘制圆形
} else if (shape->type == Square) {
// 绘制正方形
}
}
这种设计违反了开放封闭原则,因为每当添加一个新的形状,都必须修改drawShape
函数。
现在,我们用开放封闭原则来改进这个设计:
class Shape {
public:
virtual void draw() = 0; // 抽象方法
};
class Circle : public Shape {
public:
void draw() override {
// 绘制圆形
}
};
class Square : public Shape {
public:
void draw() override {
// 绘制正方形
}
};
void drawShape(Shape* shape) {
shape->draw(); // 现在是对扩展开放的
}
在这个新设计中,每当需要添加一个新的形状,只需继承Shape
类并实现draw
方法。无需修改现有的drawShape
函数或其他代码。
这样,我们就成功地遵守了开放封闭原则。这使得代码更易于维护和扩展,并降低了出错的可能性。
三、依赖倒转原则( DIP)
1、原则概述
依赖倒转原则主张高层模块不应该依赖低层模块,两者都应该依赖其抽象。
2、结构
先解释一下一些概念:
- 高层模块:可以理解为上层应用,就是业务层的实现。
- 低层模块:可以理解为底层接口,比如封装好的 API、动态库等
- 抽象:指的就是抽象类或者接口,在 C++ 中没有接口,只有抽象类
-
定义:
- 高层模块不应该依赖低层模块。两者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象(子类重写父类虚函数)。
-
目的:
- 解耦,提高系统的可维护性和可扩展性。
- 增强模块之间的可替换性。
-
应用场景:
- 当需要改变系统中的数据库、框架或者库时。
- 当需要构建多平台应用时。
- 当系统需要拥有很高的可测试性时。
-
实现方式:
- 使用抽象类或接口。
- 使用依赖注入。
3、场景举例
大聪明的项目组接了一个新项目,低层使用的是 MySQL 的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层效据的处理。而后由于某些原因,要存储到数据库的数据暴增,所以更换了0racle 数据库,由于低层的数据库接口变了,高层代码的数据库操作部分是直接调用了低层的接口,因此也需要进行对应的修改,无法实现对高层代码的直接复用,大聪明欲哭无泪。
大聪明的团队在设计系统时没有遵循依赖倒转原则,导致高层(业务逻辑)直接依赖于低层(数据库操作)的具体实现。因此,当底层数据库从MySQL变更为Oracle时,高层代码也需要相应地修改。
正确的做法:
1.定义数据库操作的抽象接口:在系统中定义一个数据库操作的抽象接口,这个接口提供一组通用的数据库操作方法,如增加(add)、删除(delete)、查找(find)和更新(update)。
class IDatabase {
public:
virtual void add() = 0;
virtual void delete() = 0;
virtual void find() = 0;
virtual void update() = 0;
};
2.实现抽象接口:MySQL和Oracle各自实现这个抽象接口。
class MySQLDatabase : public IDatabase {
// 实现 IDatabase 的方法
void add() override { /* ... */ }
void delete() override { /* ... */ }
void find() override { /* ... */ }
void update() override { /* ... */ }
};
class OracleDatabase : public IDatabase {
// 实现 IDatabase 的方法
void add() override { /* ... */ }
void delete() override { /* ... */ }
void find() override { /* ... */ }
void update() override { /* ... */ }
};
3.高层依赖于抽象:业务层只依赖于IDatabase
接口,并不关心具体是什么数据库。
class BusinessLogic {
private:
IDatabase* database;
public:
BusinessLogic(IDatabase* db) : database(db) {}
void doSomething() {
database->add();
// ...
}
};
4.使用依赖注入:在运行时,将具体的数据库实现(MySQL或Oracle)注入到业务逻辑中。
int main() {
//回顾C++语法的同族类型转换的向上转换
IDatabase* db = new OracleDatabase(); // 或者 new MySQLDatabase();
BusinessLogic bl(db);
bl.doSomething();
delete db;
return 0;
}
通过这种方式,即使底层数据库更改,高层代码(BusinessLogic
)也不需要修改。只需要在运行时注入新的数据库实现即可,实现了真正的解耦。这就是依赖倒转原则的应用。
4、里氏代换原则
依赖倒转原则的前提是需要先满足里氏代换原则:
这个原则是面向对象设计的基本原则之一,主要用于指导类的继承和多态。里氏代换原则强调,子类型必须能够替换它们的基类型,而不影响程序的正确性。
换句话说,如果S
是T
的子类型,那么在不改变程序正确性的前提下,可以用S
的对象替换T
的对象。
原则内容
- 方法覆写规则:子类可以实现父类的抽象方法,但不能覆写(override)父类的非抽象(已实现)方法。
- 数据约束扩展:子类可以增加数据属性和方法,但不能减少父类定义好的数据属性和方法。
- 不破坏类型不变量:子类维持父类已定义的类型不变性。