- 依赖倒置原则(DIP)
- 开放封闭原则(OCP)
- 里氏替换原则(LSP)
- 接口隔离原则(ISP)
- 接口隔离原则(ISP)
- 合成复用原则(CRP)
- 迪米特法则(LoD)
- 最少知识原则(LKP)
一,依赖倒置原则(DIP)
1>思想:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
2>解释:
- 类A中有类B的对象作为成员变量或局部变量,则A依赖B2.
- 类A的方法中调用了类B的方法,则A依赖B3.
- 类A继承自类B,则A依赖B4.
- 类A中有类B类型的参数或返回值,则A依赖B
3>C++代码来举一个订单支付的依赖关系例子:
假设我们有一个电商平台,包含订单模块和支付模块。不好的设计:
cpp
class Order {
public:
void pay() {
Alipay alipay;
alipay.pay();
}
}
class Alipay {
public:
void pay() {
// 支付宝支付逻辑
}
}
订单类直接依赖了支付宝Alipay类,如果要引入新的支付方式,必须修改订单类。改进设计:
cpp
class Payment {
public:
virtual void pay() = 0;
};
class Order {
private:
Payment* payment;
public:
void setPayment(Payment* p) {
payment = p;
}
void pay() {
payment->pay();
}
};
class Alipay : public Payment {
public:
void pay() override {
// 支付宝支付
}
};
class WechatPay : public Payment {
public:
void pay() override {
// 微信支付
}
};
现在订单类依赖支付接口,不依赖具体的支付类,如果要引入新支付方式,只需要新增一个支付类即可,不需要修改订单类。这种设计降低了依赖关系,提高了扩展性。
二,开放封闭原则(OCP)
1>思想:
- 对扩展开放,对更改封闭
- 软件实体(类、模块、函数等)应该是可以扩展,但是不可修改
2>解释:
- 已经完成的功能代码不需要修改,防止引入新的BUG。
- 当需要新增功能时,应该通过扩展代码来实现,而不是修改原有代码。
- 换句话说,开闭原则要求软件实体应尽量在不修改原有代码的情况下进行扩展。
- 符合开闭原则的一些手段:
. 抽象化,将可能变化的部分抽象为接口或抽象类。
. 继承,通过子类继承扩展功能。
. 多态,针对接口编程,使用虚函数和动态绑定实现运行时扩展。
. 模块化,不同功能元素分离为不同模块。
总之,开闭原则通过各种手段实现软件的扩展开放、修改关闭,降低维护成本,提高软件重用性和可维护性。它是面向对象设计原则中非常重要的一条。
3>用C++代码来举一个开闭原则的例子:
假设我们正在开发一个图形绘制软件,需要实现圆形和矩形的绘制。不好的实现:
cpp
class ShapeDrawer {
public:
void drawCircle(float x, float y, float radius) {
// 绘制圆形逻辑
}
void drawRectangle(float x, float y, float length, float width) {
// 绘制矩形逻辑
}
};
这个实现存在问题,如果需要新增图形,例如三角形,就需要修改ShapeDrawer类,违反开闭原则。我们进行改进:
cpp
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
// 绘制圆形
}
};
class Rectangle : public Shape {
public:
void draw() override {
// 绘制矩形
}
};
class ShapeDrawer {
public:
void draw(Shape* shape) {
shape->draw();
}
};
现在如果需要新增图形,只需要从Shape继承一个新的类,而无需修改ShapeDrawer,符合开闭原则。ShapeDrawer类对扩展开放(新增图形),对修改关闭。
三,单一职责原则(SRP)
1>思想:
- 一个类应该有且仅有一个引起它变化的原因
- 变化的方向隐含着类的责任
2>解释:
也就是说一个类应该只负责一项职责。
- 单一职责原则的主要优点有:
. 提高类的可读性和可维护性
. 一个类只做一件事,非常容易理解。
. 提高类的可复用性 - 职责单一的类可被重复利用。
. 降低因需求变更引起的风险 - 职责单一,需求变更影响局部。
- 遵循单一职责原则通常的一些方法:
. 根据职责拆分过于庞大的类
. 根据改变原因对类进行划分
. 用设计模式重构设计
3>用C++代码来举一个遵循单一职责原则的简单例子:
假设我们正在开发一个学生管理系统,里面有一个Student类:
cpp
class Student {
public:
void addStudent();
void deleteStudent();
void updateStudent();
void saveToFile();
void readFromFile();
};
这个Student类中既包含了学生的增删改逻辑,也包含了数据读写逻辑。它存在两个不同的变化原因:
- 学生管理功能变更
- 数据读写需求变更
按照单一职责原则,我们可以进行拆分:
cpp
class Student {
public:
void addStudent();
void deleteStudent();
void updateStudent();
};
class StudentRepository {
public:
void saveToFile();
void readFromFile();
};
现在Student类只负责学生管理,StudentRepository类负责数据读写,两个类都只有一个引起它变化的原因。这样拆分提高了代码的内聚性和可维护性。
四,里氏替换原则(LSP)
1>思想:
子类可以替换父类出现在任何地方,并且保证原有程序的正确性。
2>解释:
- 里氏替换原则的要点:
. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
. 子类可以增加自己专有的方法,但不能破坏父类的方法功能
. 子类和父类对于同一个方法的预条件要相同或宽松,后条件要相同或强化
. 任何父类可以出现的地方子类就可以出现,也就是说父类对象必须可替换为子类对象
遵循里氏替换原则是实现继承复用的基石,可以避免继承违规。总体来说,父类中的行为也适用于子类。
3>用C++代码来举一个遵循里氏替换原则的简单例子:
首先定义一个基类Shape:
cpp
class Shape {
public:
virtual void draw() = 0;
};
然后定义子类Circle:
cpp
class Circle : public Shape {
public:
void draw() override {
// 绘制圆形
}
};
再定义一个子类Rectangle:
cpp
class Rectangle : public Shape {
public:
void draw() override {
// 绘制矩形
}
};
现在我们有一个显示所有图形的函数:
cpp
void drawAllShapes(list<Shape*> shapes) {
for(auto s : shapes) {
s->draw();
}
}
这个函数接收一个Shape指针列表,通过调用draw()方法显示所有的图形。根据里氏替换原则,我们可以用子类对象Circle和Rectangle替换Shape,该函数的行为不会变化。这确保了父类和子类可以互相替换,程序行为保持一致。这样就提高了程序扩展性和维护性。这是一个基于多态实现里氏替换原则的简单示例。
五,接口隔离原则(ISP)
1>思想:
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
2>解释:
- 接口隔离原则的核心思想是:
. 接口应该细化,让接口中的方法尽可能地聚焦于单一功能
. 客户端只依赖于它需要的接口方法,避免依赖不需要的接口
-符合接口隔离原则的优点:
. 降低类间耦合度提高系统内聚性
. 更好地复用性和可维护性
-以下几点可以帮助设计符合接口隔离原则的接口:
. 为特定客户端建立专用接口
. 将庞大复杂接口分解为多个粒度小的接口
. 自足接口只提供客户端需要的方法,不多不少
总之,接口隔离原则通过精心设计接口,建立最少和高内聚的接口,来减少依赖,降低耦合,提高灵活性。
3>用C++代码来举一个接口隔离原则的例子:
假设我们定义了一个打印机接口:
cpp
class IPrinter {
public:
virtual void print(Document& doc) = 0;
virtual void fax(Document& doc) = 0;
virtual void scan(Document& doc) = 0;
};
这个接口包含了打印、传真和扫描等多种功能。但是作为客户端,我只需要使用打印机的打印功能:
cpp
class Client {
private:
IPrinter* printer;
public:
Client(IPrinter* p) {
printer = p;
}
void printPage(Document& doc) {
printer->print(doc);
}
};
这违反了接口隔离原则 - 客户端被迫依赖于它不需要的接口。我们可以将接口拆分:
cpp
class IPrinter {
virtual void print(Document& doc) = 0;
};
class IScanner {
public:
virtual void scan(Document& doc) = 0;
};
class Client {
private:
IPrinter* printer;
public:
//...
};
这样客户端只依赖于实际需要的IPrinter接口,符合接口隔离原则,也提高了系统的内聚性。
六,合成复用原则(CRP)
1>思想:
优先使用对象组合,而不是继承
2>解释:
- 继承在某一程度上破坏了代码封装性,子类与父类代码耦合度高
- 而对象组合则只要求被组合的对象具有被良好定义的接口,耦合度低
- 合成复用的一些优点:
. 继承是静态的,而合成是动态的。合成关系可以在运行时选择,更灵活
. 合成方式复用无须使新的类承担不必要的方法和数据
. 新类无须理解基类实现细节,减少依赖,降低耦合但是继承也是非常重要的复用关系,关键是要权衡利弊,适当使用。
- 一句话总结合成复用原则:优先考虑使用合成/聚合方式实现复用,而不是直接使用继承。这可以避免不必要的耦合性,提高系统的灵活性和可维护性。
3> 用C++代码来举一个使用合成复用原则的例子:
假设我们要设计一个工资管理系统,下面是使用继承设计的方式:
cpp
class Employee {
public:
void calculateSalary();
};
class Manager : public Employee {
public:
void calculateSalary() {
// 管理者工资计算逻辑
}
};
class Sales : public Employee {
public:
void calculateSalary() {
// 销售员工资计算逻辑
}
};
这种设计中,Manager和Sales都继承自Employee,复用了calculateSalary方法。现在我们考虑使用合成方式设计:
cpp
class Employee {
//...
};
class SalaryCalculator {
public:
void calculateSalary(Employee* emp) {
// 工资计算逻辑
}
};
class Manager {
//...
SalaryCalculator calculator;
};
class Sales {
//...
SalaryCalculator calculator;
};
这样Manager和Sales与Employee没有继承关系,但都持有SalaryCalculator,来实现工资计算逻辑的复用。这避免了不必要的继承,类之间解耦,也更灵活。所以合成复用原则提倡优先考虑使用合成或聚合来复用功能,而不是直接使用继承。
七,迪米特法则(LoD)
1>思想:
- 一个对象应该对其他对象保持最少的了解。
2>解释:
- 迪米特法则的主要思想是:
. 一个类应该对自己需要耦合或调用的类知道的越少越好
- 只与朋友交流,不和陌生类讲话迪米特法则的具体要点:
. 只调用当前对象的方法
. 只调用方法参数对象的方法
. 不调用全局变量的方法
- 不调用实例变量的方法,除非是在方法中创建的遵循迪米特法则的好处是:
. 降低类之间的耦合度
. 提高类的内聚力
提高代码可维护性但是也不能过度使用,否则会导致设计中出现过多中介类。总之,迪米特法则提倡通过限制类间交流来控制耦合,但也需要灵活运用。
3>用C++代码来举一个迪米特法则的简单例子:
cpp
class Student {
public:
void study() {
// 学习
}
};
class Teacher {
public:
void teach(Student* student) {
student->study(); // 直接调用Student的方法
}
};
class Course {
public:
Student* student;
void takeCourse() {
Teacher teacher;
teacher.teach(student); // 通过方法参数调用
}
};
int main() {
Course course;
Teacher teacher;
Student student;
course.takeCourse();
return 0;
}
在这个例子中:
. Teacher类直接调用了Student类的study()方法,违反了迪米特法则
. Course类通过方法参数teacher调用Teacher的teach()方法,这符合迪米特法则Teacher类直接调用Student的方法增加了耦合,违反了“只与直接的朋友交互”的原则。
所以迪米特法则提倡通过方法参数等间接的方式调用其他对象的方法,而不是直接调用,这样可以降低耦合度。
我们可以这样修改Teacher类,遵循迪米特法则:
cpp
class Teacher {
public:
void teach(Student* student) {
// 教学逻辑
student->listen(); // 调用Student的方法
}
};
class Student {
public:
void listen() {
// 认真听讲
}
void study() {
// 学习
}
};
修改后的Teacher类不再直接调用Student的study()方法,而是通过添加一个listen()方法,让Student类自己负责响应教学。Teacher类只需要告诉Student去“听讲”,具体的学习行为由Student类自己实现。
这样Teacher和Student类之间的耦合度就降低了,Teacher类不需要了解Student类的具体实现。
另外,如果需要的话,我们可以在Course类中定义一个study()方法:
cpp
class Course {
public:
void study() {
student->study();
}
}
并在Teacher的teach()方法中调用course->study(),��让Student进行学习。
这种通过方法参数来间接调用的方式更加松耦合,也更贴合迪米特法则。
八,最少知识原则(LKP)
1>思想:
- 一个软件实体应当尽可能少地与其他实体发生相互作用。
2>解释:
- 其核心思想是:
. 每个类尽量减少对其他类的了解
- 只与需要直接通信的类发生依赖关系优点:
. 降低系统的耦合度
. 增加模块的独立性、可移植性和可复用性
- 提高系统的可维护性和扩展性遵循最少知识原则的具体方法:
.不在类中使用不需要的类
. 方法参数应该具体化,避免使用基类类型
. 不在方法中使用可被替换的类
. 尽量缩小变量作用域
. 使用private封装细节但是也不能过度使用,否则会出现过多的中介类。
总之,最少知识原则通过使每个类对系统的其它部分都一无所知,来降低耦合提高内聚,是很重要的面向对象设计原则。
3>用C++代码来举一个遵循最少知识原则的例子:
cpp
// 违反最少知识原则 - Rectangle类知道Point类的具体实现
class Rectangle {
private:
Point m_leftTop;
Point m_rightBottom;
public:
// 业务方法
};
// 优化后 - Rectangle类只依赖于抽象的PointInterface类
class PointInterface {
public:
virtual int x() = 0;
virtual int y() = 0;
};
class Rectangle {
private:
PointInterface* m_leftTop;
PointInterface* m_rightBottom;
public:
// 业务方法
};
class Point : public PointInterface {
// 点类实现
};
优化前,Rectangle类直接依赖并使用了Point类,这违反了最少知识原则。
优化后,引入了抽象的PointInterface类,Rectangle类只依赖于这个接口,而不是Point的具体实现。
这样我们可以用不同的点类实现替换Point,Rectangle类不需要改变。
这减少了 Rectangle 对系统的依赖,提高了其独立性和可扩展性。
通过定义抽象接口约束类间交互,可以遵循最少知识原则,构建松耦合的系统。