单一职责原则(Single Responsibility Principle, SRP)
定义:一个类应该只有一个引起变化的原因。
解释:每个类应该仅仅负责一项职责或功能。这样可以使类更加简洁,并且在需求变更时,修改某一项职责的代码不会影响到其他职责的代码,从而提高系统的可维护性。
里氏替换原则(Liskov Substitution Principle, LSP)
定义:子类对象必须能够替换掉所有父类对象。
解释:子类应该能够替换其父类,并且子类的行为应与父类一致。即,使用基类的地方必须能够透明地使用其子类对象,而不会引发错误或行为变化。
示例:
#include <iostream>
// 违反LSP,子类行为不符合父类预期
class Bird {
public:
virtual void fly() {
std::cout << "Bird is flying\n";
}
};
// 麻雀类
class Sparrow : public Bird {
public:
void fly() override {
std::cout << "Sparrow is flying\n";
}
};
// 鸵鸟类,鸵鸟不会飞
class Ostrich : public Bird {
public:
void fly() override {
throw std::logic_error("Ostriches can't fly");
}
};
// 遵循LSP,设计更通用的接口
class Bird {
public:
virtual void move() {
std::cout << "Bird is moving\n";
}
};
class Sparrow : public Bird {
public:
void move() override {
std::cout << "Sparrow is flying\n";
}
};
class Ostrich : public Bird {
public:
void move() override {
std::cout << "Ostrich is running\n";
}
};
int main() {
Bird* bird = new Sparrow();
bird->move();
bird = new Ostrich();
bird->move();
delete bird;
return 0;
}
开放封闭原则(Open-Closed Principle, OCP)
具体解释
- 对扩展开放:软件实体应当可以通过新增功能来扩展其行为,而无需修改现有代码。这意味着当需求变化或增加时,我们应当能够通过增加新代码(如新类或新方法)来满足这些需求,而不是修改已有的代码。
- 对修改封闭:一旦软件实体被开发完成并投入使用,就不应再去修改它。修改现有代码可能会引入新的错误,并破坏现有系统的稳定性。因此,在设计时应尽量考虑到可能的扩展需求,确保以后只需要增加新代码即可满足新的需求。
实现方法
为了实现开放封闭原则,常用的方法有以下几种:
- 使用抽象和继承:通过定义抽象类或接口,并将具体实现延迟到子类中。这样可以在不修改抽象类的情况下,通过增加新的子类来实现功能的扩展。
- 使用多态:多态允许程序在运行时选择合适的实现,从而提高系统的灵活性和扩展性。例如,通过接口引用具体实现类的实例,客户端代码只需依赖接口而不依赖具体实现类。
- 使用装饰者模式(Decorator Pattern) :这种设计模式允许通过组合对象来动态地扩展对象的功能,而无需修改现有类的代码。
示例
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
// 抽象基类 Shape
class Shape {
public:
virtual double area() const = 0; // 纯虚函数,计算面积
virtual ~Shape() = default; // 虚析构函数
};
// 圆形类 Circle,实现了 Shape
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
double area() const override {
return M_PI * radius * radius; // 计算圆形面积
}
private:
double radius;
};
// 矩形类 Rectangle,实现了 Shape
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width(width), height(height) {}
double area() const override {
return width * height; // 计算矩形面积
}
private:
double width, height;
};
// 新增一个三角形类 Triangle,实现了 Shape
class Triangle : public Shape {
public:
Triangle(double base, double height) : base(base), height(height) {}
double area() const override {
return 0.5 * base * height; // 计算三角形面积
}
private:
double base, height;
};
int main() {
// 创建不同形状的对象
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(std::make_unique<Triangle>(4.0, 7.0));
// 计算并输出每个形状的面积
for (const auto& shape : shapes) {
std::cout << "Area: " << shape->area() << std::endl;
}
return 0;
}
-
抽象基类
Shape:- 定义了一个纯虚函数
area(),使得所有派生类必须实现这个方法。
- 定义了一个纯虚函数
-
具体形状类
Circle、Rectangle和Triangle:- 每个类都实现了
Shape类的area()方法来计算其面积。 - 这样,当我们需要增加一个新形状时,只需创建一个新的类继承
Shape并实现area()方法即可。
- 每个类都实现了
这种设计方式使得系统对扩展开放(可以轻松地添加新形状),对修改封闭(不需要修改现有的类和代码)。
依赖倒置原则(Dependency Inversion Principle, DIP)
定义:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
解释:通过依赖于抽象(接口或抽象类)而不是具体实现,可以使高层模块和低层模块之间的依赖关系变得更加稳定,从而提高系统的可维护性和扩展性。
违反DIP的例子
#include <iostream>
// 具体实现类 LightBulb
class LightBulb {
public:
void turnOn() {
std::cout << "LightBulb turned on\n";
}
};
// 高层模块 Switch 依赖于具体的 LightBulb 类
class Switch {
public:
Switch(LightBulb* bulb) : bulb(bulb) {}
void operate() {
bulb->turnOn();
}
private:
LightBulb* bulb;
};
int main() {
LightBulb bulb;
Switch sw(&bulb);
sw.operate(); // 输出: LightBulb turned on
return 0;
}
在这个例子中,Switch 类直接依赖于 LightBulb 类,这违反了DIP,因为高层模块(Switch)依赖于低层模块(LightBulb)。
遵循DIP的例子
#include <iostream>
// 抽象接口 Switchable
class Switchable {
public:
virtual void turnOn() = 0;
virtual ~Switchable() = default;
};
// 具体实现类 LightBulb 实现了 Switchable 接口
class LightBulb : public Switchable {
public:
void turnOn() override {
std::cout << "LightBulb turned on\n";
}
};
// 高层模块 Switch 依赖于抽象接口 Switchable
class Switch {
public:
Switch(Switchable* device) : device(device) {}
void operate() {
device->turnOn();
}
private:
Switchable* device;
};
int main() {
LightBulb bulb;
Switch sw(&bulb);
sw.operate(); // 输出: LightBulb turned on
return 0;
}
-
抽象接口
Switchable:- 定义了一个纯虚函数
turnOn(),所有实现这个接口的类都必须提供这个方法的具体实现。
- 定义了一个纯虚函数
-
具体实现类
LightBulb:- 实现了
Switchable接口,并提供了turnOn()方法的具体实现。
- 实现了
-
高层模块
Switch:- 依赖于
Switchable接口,而不是具体的LightBulb类。 Switch类的构造函数接受一个Switchable类型的指针,并调用turnOn()方法。
- 依赖于
总结
依赖倒置原则通过引入抽象接口,使高层模块和低层模块之间的耦合度降低,提高了系统的灵活性和可维护性。通过这种方式,我们可以轻松地扩展系统,而无需修改现有的高层模块。
接口隔离原则(Interface Segregation Principle, ISP)
定义:客户端不应该被迫依赖于它们不使用的方法。换句话说,一个类对另一个类的依赖应该建立在最小的接口上。
解释:接口隔离原则旨在避免大而臃肿的接口,确保接口尽量小,只包含客户端真正需要的方法。这样可以提高系统的灵活性和可维护性。
违反ISP的例子
#include <iostream>
// 大而臃肿的接口 Worker
class Worker {
public:
virtual void work() = 0;
virtual void eat() = 0;
};
// HumanWorker 需要实现 work 和 eat 方法
class HumanWorker : public Worker {
public:
void work() override {
std::cout << "Human working\n";
}
void eat() override {
std::cout << "Human eating\n";
}
};
// RobotWorker 需要实现 work 方法,但不需要 eat 方法
class RobotWorker : public Worker {
public:
void work() override {
std::cout << "Robot working\n";
}
void eat() override {
// 机器人不需要吃饭
throw std::logic_error("Robots don't eat");
}
};
int main() {
HumanWorker human;
RobotWorker robot;
human.work();
human.eat();
robot.work();
try {
robot.eat();
} catch (const std::logic_error& e) {
std::cerr << e.what() << '\n';
}
return 0;
}
在这个例子中,RobotWorker类不需要实现eat方法,但由于Worker接口强制要求,它仍然需要提供一个实现。这违反了ISP。
遵循ISP的例子
#include <iostream>
// 单一职责接口 Workable
class Workable {
public:
virtual void work() = 0;
virtual ~Workable() = default;
};
// 单一职责接口 Eatable
class Eatable {
public:
virtual void eat() = 0;
virtual ~Eatable() = default;
};
// HumanWorker 需要实现 Workable 和 Eatable 接口
class HumanWorker : public Workable, public Eatable {
public:
void work() override {
std::cout << "Human working\n";
}
void eat() override {
std::cout << "Human eating\n";
}
};
// RobotWorker 只需要实现 Workable 接口
class RobotWorker : public Workable {
public:
void work() override {
std::cout << "Robot working\n";
}
};
int main() {
HumanWorker human;
RobotWorker robot;
human.work();
human.eat();
robot.work();
return 0;
}
-
单一职责接口
Workable和Eatable:Workable接口只包含work方法。Eatable接口只包含eat方法。
-
具体实现类
HumanWorker和RobotWorker:HumanWorker类实现了Workable和Eatable接口,因为它需要同时工作和吃饭。RobotWorker类只实现了Workable接口,因为它只需要工作,不需要吃饭。
总结
通过将大接口拆分为多个小接口,每个接口只包含一个具体职责的方法,我们可以确保每个类只依赖于它实际需要的方法,从而遵循接口隔离原则。这种设计方式提高了系统的灵活性和可维护性,使得系统更容易扩展和修改。
迪米特法则(Law of Demeter,LoD)
迪米特法则(Law of Demeter,LoD),也称为最少知识原则(Principle of Least Knowledge),是一种软件设计原则。其核心思想是一个对象应当尽可能少地了解其他对象,以减少耦合度,提高系统的模块化和可维护性。
迪米特法则的优点
- 降低耦合度:通过减少对象之间的直接依赖,可以降低类与类之间的耦合度,提高系统的灵活性。
- 提高模块化:遵守迪米特法则可以使系统更加模块化,各个模块之间互不干涉,便于维护和扩展。
- 增强可读性:减少对象之间的交互,使代码更加清晰,容易理解。