面向对象设计原则(SOLID)
各位同学好。今天我们来聊聊 SOLID 原则。这五个原则,说白了就是面向对象设计的「基本功」。我做了十几年架构,见过太多代码因为违反这些原则而变得难以维护。嗯,咱们一个一个来看。
单一职责原则(SRP)
单一职责原则,英文是 Single Responsibility Principle。它的核心思想很简单:一个类只应该有一个引起它变化的原因。
什么意思呢?我举个例子。你写了一个 OrderService 类,里面既有订单计算逻辑,又有数据库保存逻辑,还有发送邮件通知的逻辑。这时候,如果订单计算规则变了,你要改这个类;如果数据库表结构变了,你也要改这个类;如果邮件模板变了,你还要改这个类。一个类被三个不同的原因牵着走,这就是典型的违反 SRP。
核心要点:每个类只负责一件事。如果一件事变了,只改一个类。
我在项目中遇到过这样一个案例:一个 ReportGenerator 类,既负责从数据库取数据,又负责生成 HTML 报表,还负责把报表通过 FTP 发送出去。后来 FTP 服务器换了地址,改代码的时候不小心把报表格式也改坏了。这就是职责耦合带来的问题。
开闭原则(OCP)
开闭原则,Open-Closed Principle。说的是:对扩展开放,对修改关闭。
说白了就是:你想加新功能,别去改已有的代码,而是通过扩展来实现。我刚开始写代码时也不理解,觉得改代码多直接啊。后来发现,改一个地方,可能引发连锁反应,测试全得重来。
举个例子:
// 违反 OCP 的设计
public class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
}
// 每加一种形状,就要加一个 else if
}
}
// 符合 OCP 的设计
public interface Shape {
double area();
}
public class Circle implements Shape {
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
public double area() {
return width * height;
}
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.area(); // 不需要改任何代码
}
}
你看,第二种设计,加新形状只需要新增一个类,AreaCalculator 完全不用动。这就是开闭原则的威力。
里氏替换原则(LSP)
里氏替换原则,Liskov Substitution Principle。定义有点绕:子类对象必须能够替换掉父类对象,并且程序的行为不会发生变化。
我换个说法:你写代码时,如果用了父类的地方,换成子类也能正常工作,那就对了。如果不行,说明你的继承关系有问题。
我记得有个经典的反例:正方形继承矩形。
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 正方形宽高相等
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
看起来没问题?你试试这个场景:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
// 用户期望 width=5, height=10
// 实际得到 width=10, height=10
这就是违反 LSP 的典型例子。子类改变了父类的行为约定。我曾经在一个支付系统中见过类似问题:子类重写了父类的 pay() 方法,结果把支付流程的顺序改了,导致对账系统崩溃。嗯,教训深刻。
接口隔离原则(ISP)
接口隔离原则,Interface Segregation Principle。意思是:客户端不应该被迫依赖它不需要的接口。
说白了,接口要小而专,不要大而全。你想想看,如果一个接口有 10 个方法,你的实现类只需要其中 2 个,那另外 8 个方法你就得写空实现或者抛异常。这多别扭。
我建议这样设计:
// 违反 ISP:一个臃肿的接口
public interface Worker {
void work();
void eat();
void sleep();
}
// 符合 ISP:拆分成多个小接口
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// 机器人只需要实现 Workable
public class Robot implements Workable {
public void work() { /* 工作逻辑 */ }
}
// 人类可以实现所有三个接口
public class Human implements Workable, Eatable, Sleepable {
public void work() { /* 工作逻辑 */ }
public void eat() { /* 吃饭逻辑 */ }
public void sleep() { /* 睡觉逻辑 */ }
}
小技巧:如果你发现一个接口中有很多方法,而且不同实现类只用到其中一部分,那就该考虑拆分接口了。
依赖倒置原则(DIP)
依赖倒置原则,Dependency Inversion Principle。核心是:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
这句话有点绕。我换个说法:你的业务逻辑代码,不要直接依赖具体的数据库、具体的文件系统、具体的第三方服务。而是依赖接口。这样换数据库、换服务的时候,业务代码不用动。
举个例子:
// 违反 DIP:高层直接依赖低层
public class UserService {
private MySQLDatabase db = new MySQLDatabase(); // 直接依赖具体实现
public void saveUser(User user) {
db.save(user);
}
}
// 符合 DIP:依赖抽象
public interface UserRepository {
void save(User user);
}
public class MySQLUserRepository implements UserRepository {
public void save(User user) { /* MySQL 实现 */ }
}
public class UserService {
private UserRepository repo; // 依赖抽象
public UserService(UserRepository repo) {
this.repo = repo; // 具体实现由外部注入
}
public void saveUser(User user) {
repo.save(user);
}
}
我曾经接手过一个项目,所有业务代码都直接 new 了具体的数据库连接。后来要从 MySQL 迁移到 PostgreSQL,几乎每个文件都要改。那叫一个痛苦。从那以后,我写代码一定会先定义接口,再写实现。
知识体系总览
下面这张图,把 SOLID 五个原则的核心思想和它们之间的关系画出来了。你可以保存下来,写代码前看一眼。
五个原则对比
| 原则 | 核心思想 | 违反后果 | 我常用的实践 |
|---|---|---|---|
| SRP | 一个类只做一件事 | 类臃肿,修改牵一发动全身 | 先写职责清单,再拆分 |
| OCP | 扩展开放,修改关闭 | 加功能要改旧代码,风险高 | 多用接口和抽象类 |
| LSP | 子类可替换父类 | 继承关系混乱,程序行为异常 | 慎用继承,优先组合 |
| ISP | 接口要小而专 | 实现类被迫实现无用方法 | 接口方法不超过 5 个 |
| DIP | 依赖抽象,不依赖具体 | 换底层实现要改高层代码 | 构造函数注入接口 |
避坑指南:我曾经在一个项目中,为了「灵活」把每个类都拆成了接口+实现,结果接口数量爆炸,反而增加了维护成本。SOLID 原则是指导方针,不是教条。要根据实际情况灵活运用,不要为了用原则而用原则。
好了,SOLID 五个原则就讲到这里。记住一句话:好的设计不是一蹴而就的,而是在不断重构中打磨出来的。写代码时多想想这几个原则,慢慢就会形成习惯。