SOLID 原则:编写可维护的面向对象代码
面向对象 SOLID 原则:编写可维护的面向对象代码
什么是 SOLID 原则?
SOLID 是面向对象编程和设计中五个基本原则的缩写,由 Robert C. Martin(Uncle Bob)提出并推广。这五个原则共同构成了一套编写可维护、可扩展、低耦合、高内聚代码的指南。当你的项目越变越大时,遵循这些原则可以有效减少“牵一发而动全身”的风险,让代码更容易理解和修改。
五个字母分别代表:
- Single Responsibility Principle(单一职责原则)
- Open/Closed Principle(开闭原则)
- Liskov Substitution Principle(里氏替换原则)
- Interface Segregation Principle(接口隔离原则)
- Dependency Inversion Principle(依赖倒置原则)
单一职责原则(SRP)
定义
一个类应该只有一个引起它变化的原因。
换句话说,一个类只负责完成一个明确的功能,如果它因为多个不相关的理由需要被修改,那就说明这个类的职责不够单一。
为什么重要?
- 提高类的内聚性,降低耦合。
- 修改一个功能时不会意外影响其他不相关功能。
- 类更小、更易读、更易测试。
违反 SRP 的例子
class Report {
private String content;
public void generateReport() {
// 生成报告内容
}
public void saveToFile(String filename) {
// 将报告保存到文件系统
}
public void sendByEmail(String email) {
// 通过邮件发送报告
}
}
在这个类中,Report 负责了三件不同的事情:生成内容、持久化存储和发送通知。未来如果文件存储方式改变(比如从本地文件变为云存储),或者邮件发送逻辑需要调整,都会迫使修改这个原本只关注“报告内容生成”的类。
遵循 SRP 的重构
class Report {
private String content;
public void generateReport() {
// 生成报告内容
}
}
class ReportPersistence {
public void saveToFile(Report report, String filename) {
// 保存报告到文件
}
}
class ReportMailer {
public void sendByEmail(Report report, String email) {
// 发送报告邮件
}
}
现在每个类只负责一件事,任何一方的变化都被隔离在各自的类中。
开闭原则(OCP)
定义
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
当需求变化时,应该通过添加新代码(扩展)来适应变化,而不是去修改已经存在并测试通过的代码。
实现方式
通常通过抽象和继承/接口来实现:定义一个稳定的抽象,让不同的行为实现这个抽象,从而在不改动原有代码的基础上扩展功能。
违反 OCP 的例子
class AreaCalculator {
public double area(Object shape) {
if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
}
return 0;
}
}
每当需要支持一种新图形(例如三角形)时,都必须修改 AreaCalculator,增加 else if 分支。这违背了“对修改关闭”。
遵循 OCP 的重构
interface Shape {
double area();
}
class Rectangle implements Shape {
double width, height;
public double area() { return width * height; }
}
class Circle implements Shape {
double radius;
public double area() { return Math.PI * radius * radius; }
}
class AreaCalculator {
public double area(Shape shape) {
return shape.area(); // 只需调用抽象方法
}
}
现在增加三角形只需要新建一个实现 Shape 的类,而 AreaCalculator 完全不需要改变。
里氏替换原则(LSP)
定义
所有引用基类的地方必须能够透明地使用其子类的对象,而不会产生任何错误或异常。
简单来说:子类必须能真正替代父类,继承关系应该是“是一个(is-a)”的语义强化,而不是削弱。
违反 LSP 的典型情况
- 子类重写父类方法并抛出父类未声明的异常。
- 子类重写方法时修改了父类方法原本的含义或行为约束(如前置条件更强、后置条件更弱)。
- 经典的“矩形—正方形”问题。
class Rectangle {
private int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // 强制让宽高相等
}
@Override
public void setHeight(int h) {
super.setWidth(h);
super.setHeight(h);
}
}
现在如果有一个方法接收 Rectangle 参数:
void changeSize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.getArea() == 50; // 对矩形成立
}
当传入 Square 对象时,setWidth(5) 会让宽和高都变成 5,setHeight(10) 又会让它们都变成 10,最终面积是 100,断言失败。正方形不是完全意义上的矩形,因此不应该用继承来实现。
遵循 LSP 的重构
解决方案之一是取消继承关系,让正方形不再继承矩形,或者两者都从更抽象的接口继承。如果不能确保完美的“is-a”关系,宁可使用组合而非继承。
接口隔离原则(ISP)
定义
客户端不应该被强迫依赖于它不使用的方法。
一个臃肿的接口往往会被不同客户端只使用其中一部分功能,修改这个接口会影响到所有客户端,即便它们不关心那部分改变。
典型坏味道
一个“万能”的接口:
interface Worker {
void work();
void eat();
void sleep();
}
如果有一个 Robot 类实现了 Worker,它并不需要 eat() 和 sleep(),却不得不提供空实现或抛出异常,这就破坏了 ISP。
遵循 ISP 的重构
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
class Robot implements Workable {
public void work() { /* ... */ }
}
现在客户端可以根据自己的需要只依赖 Workable 接口,不再被无用的方法污染。
依赖倒置原则(DIP)
定义
高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
- 高层模块:实现业务逻辑的策略或用例。
- 低层模块:具体的实现细节,如数据库操作、第三方库调用、文件读写等。
传统编程中高层模块直接调用低层模块,导致高层模块难以替换底层实现。DIP 要求我们反转依赖方向,让高层模块定义接口(抽象),低层模块去实现这些接口。
违反 DIP 的例子
class LightBulb {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
class Switch {
private LightBulb bulb;
public Switch(LightBulb bulb) {
this.bulb = bulb;
}
public void operate() {
// ... toggle
}
}
Switch 直接依赖于具体的 LightBulb,如果将来想控制 Fan 或其他设备,必须修改 Switch。
遵循 DIP 的重构(依赖注入)
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
class Fan implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void operate() {
// 调用 device.turnOn() 或 turnOff()
}
}
现在 Switch 只依赖抽象 Switchable,任何实现了该接口的设备都可以被控制,改动低层细节不会影响高层开关逻辑。
总结
SOLID 原则不是僵化的教条,而是帮助你应对软件复杂度的指导方针。
- SRP 让你的类更小、更专注,减少修改的理由。
- OCP 让代码通过扩展引入新特性,而非冒险修改稳定代码。
- LSP 保证继承体系的可用性,避免子类破坏程序的正确性。
- ISP 避免臃肿接口,让客户端只依赖自己需要的方法。
- DIP 将代码从具体实现中解耦,使得系统能更容易适应变化。
将这些原则组合使用,能够设计出健壮、灵活且易于维护的面向对象系统。日常开发中不必追求100%“纯净”,但时常自问:“这个设计是否能让未来变更更轻松?”会帮助你逐步内化这些思想。