- The Single Responsibility Principle (SRP): 单一责任原则
- The Open-Closed Principle (OCP): 开放 - 封闭原则
- The Liskov Substitution Principle (LSP): Liskov 替换原则
- The Interface Segregation Principle (ISP): 接口隔离原则
- The Dependency Inversion Principle (DIP): 依赖转置原则
单一责任原则(SRP)
- 责任的含义:变化的原因(a reason for change)。
- SRP 的核心在于:一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。
当一个类包含了多个责任时,会引起以下不良后果:
- 代码难以理解和维护: 包含多个责任的类会导致代码逻辑复杂,不同责任的代码相互交织在一起,使得代码难以理解和维护。当需要修改其中一个责任时,可能会影响到其他责任,增加了修改的风险和成本。
- 耦合性增加: 不同责任的代码逻辑耦合在一起,使得类与其他类之间的耦合性增加。这会导致类的复用性降低,因为修改一个责任可能会影响到其他相关的功能,从而限制了类的灵活性。
- 难以测试: 包含多个责任的类通常会有更多的方法和行为,这会增加测试的复杂度。因为每个责任都可能需要不同的测试情况和测试数据,导致测试变得困难和耗时。
考虑一个描述汽车的类,它应该只关注汽车的属性和行为,而不应该包含关于维修或销售汽车的逻辑。
class Car {
private String model;
private int year;
// Constructor, getters, and setters
public void startEngine() {
// Code to start the engine
}
public void accelerate() {
// Code to accelerate the car
}
}
class CarRepairShop {
public void repairCar(Car car) {
// Code to repair the car
}
}
在这个示例中,Car
类只负责描述汽车的属性和行为,而 CarRepairShop
类负责维修汽车。如果将 repair()
方法在 Car
中实现,会导致类的职责不清晰,增加了类的复杂性和耦合性。
开放 - 封闭原则(OCP)
Classes should be:
- open for extension (对扩展的开放)
- but closed for modification (对修改的封闭)
考虑两个类,圆形(Circle
)和矩形(Rectangle
)。AreaCalculator
用于计算面积。
class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() {
return width * height;
}
}
class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return circle.area();
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.area();
}
return 0;
}
}
在上述示例中,如果需要新增一种形状,如三角形,那么需要修改 AreaCalculator.calculateArea()
,维护分支语句,较为繁琐。违背了 OCP。
我们希望遵循 OCP,在添加新的形状类型的同时,而不修改已有的代码,关键技术在于使用抽象。
考虑增添一个形状(Shape
)接口(或者抽象类),圆形(Circle
)和矩形(Rectangle
)分别继承这个接口。
interface Shape {
double area();
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
class Circle implements Shape {
private double radius;
// Constructor, getters, and setters
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width;
private double height;
// Constructor, getters, and setters
@Override
public double area() {
return width * height;
}
}
// 新增一个三角形类,不需要修改已有代码
class Triangle implements Shape {
private double base;
private double height;
// Constructor, getters, and setters
@Override
public double area() {
return 0.5 * base * height;
}
}
在上述示例中,我们新增一个三角形(Triangle
)类,不需要修改已有代码。
Liskov 替换原则(LSP)
子类型必须能够替换其基类型而不影响程序的正确性。
考虑一个图形类的层次结构,其中包含了正方形(Square
)和矩形(Rectangle
),正方形是矩形的子类。根据 LSP,子类正方形应该能够替换父类矩形而不影响程序的正确性。
class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int area() {
return width * height;
}
}
class Square extends Rectangle {
public Square(int size) {
super(size, size);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
接口隔离原则(ISP)
一个类不应该强迫它的客户端依赖于它们不需要使用的方法,即:只提供必要的接口。
ISP 要求我们不应设计 " 胖 " 接口," 胖 " 接口会导致不够聚合。
- " 胖 " 接口应该分解为多个小的接口
- 不同的接口向不同的客户端提供服务
- 客户端只访问自己所需要的端口
考虑一个打印机(Printer
)接口,可能包含打印 (print()
) 和扫描 (scan()
) 功能。
interface Printer {
void print();
void scan();
}
class SimplePrinter implements Printer {
@Override
public void print() {
// Code to print
}
@Override
public void scan() {
throw new UnsupportedOperationException("This printer does not support scanning");
}
}
class MultiFunctionPrinter implements Printer {
@Override
public void print() {
// Code to print
}
@Override
public void scan() {
// Code to scan
}
}
在上述示例中,SimplePrinter
类并不支持 scan()
功能,但却实现了 Printer
接口,进而违背了 ISP。
为了分解功能,我们重新设计两个接口 Printable
与 Scanable
。SimplePrinter
只实现 Printable
,而 MultiFunctionPrinter
同时实现 Printable
与 Scanable
。
interface Printable {
void print();
}
interface Scanable {
void scan();
}
class SimplePrinter implements Printable {
@Override
public void print() {
// Code to print
}
}
class MultiFunctionPrinter implements Printable, Scanable {
@Override
public void print() {
// Code to print
}
@Override
public void scan() {
// Code to scan
}
}
依赖转置原则(DIP)
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
考虑一个简单的电灯控制器(LightSwitch
),能控制灯(Light
)的开关。朴素的想法是让其直接依赖于 Light
,如下代码所示:
class Light {
public void turnOn() {
// Code to turn on light
}
public void turnOff() {
// Code to turn off light
}
}
class LightSwitch {
private Light light;
public LightSwitch(Light light) {
this.light = light;
}
public void turnOn() {
light.turnOn();
}
public void turnOff() {
light.turnOff();
}
}
但是如果未来需求不只是控制电灯(Light
),还需要控制 LED,白炽灯等。则需要修改 LightSwitch
的相关方法,改动分支语句,较为繁琐。这违背了 DIP。
为了改进,我们应该让 LightSwitch
不依赖于具体类 Light
,而是抽象类或接口。我们设计一个 Switchable
接口,让 LightSwitch
直接依赖于这个抽象接口。
interface Switchable {
void turnOn();
void turnOff();
}
class Light implements Switchable {
@Override
public void turnOn() {
// Code to turn on light
}
@Override
public void turnOff() {
// Code to turn off light
}
}
class LightSwitch {
private Switchable device;
public LightSwitch(Switchable device) {
this.device = device;
}
public void turnOn() {
device.turnOn();
}
public void turnOff() {
device.turnOff();
}
}
此时,当再次面对变化时,例如增加一个照明设备,我们只需要新增一个类,实现 Switchable
即可。