SOLID 是面向对象设计的五个原则,每一个都是"避免某类坏味道"的经验总结。这篇用坏例子 → 好例子 → 一句话记住的方式把五个讲透(Java 示例,全部编译验证过)。
一、S:单一职责(Single Responsibility)
一个类只干一件事——一个类只有一个"需要修改它的理由"。
java
// ❌ 坏:一个类干了三件事(用户业务 + 发邮件 + 存 Excel)
class UserService {
void createUser(String name) { /* 业务 */ }
void sendWelcomeEmail(String email) { /* 邮件 */ }
void exportUserExcel() { /* Excel */ }
}
// 改邮件逻辑要动这个类,改 Excel 也要动——牵一发动全身
// ✅ 好:三个类各管一件事
class UserService { void createUser(String name) { } } // 只管业务
class EmailService { void sendWelcomeEmail(String email) { } } // 只管邮件
class ExcelExporter { void exportUserExcel() { } } // 只管导出一句话记住:"一个类一个理由去改"——职责混在一起,改 A 容易碰坏 B。
二、O:开闭原则(Open-Closed)
对扩展开放,对修改关闭——加新功能时不改老代码,而是扩展新代码。
java
// ❌ 坏:加新优惠要改老方法(if-else 越堆越多)
class DiscountService {
double calc(double price, int type) {
if (type == 1) return price * 0.9;
if (type == 2) return price * 0.8;
return price; // 加 type==3 又要改这里!
}
}
// ✅ 好:策略接口 + 实现类,加新优惠 = 新增一个类,老代码不动
interface Discount {
double calc(double price);
}
class VipDiscount implements Discount { public double calc(double p) { return p * 0.9; } }
class GoldDiscount implements Discount { public double calc(double p) { return p * 0.8; } }
// 新优惠:class NewDiscount implements Discount { ... } ← 只加不改一句话记住:"加功能别改老代码"——这是策略模式(如有多篇)和模板方法的根基。
三、L:里氏替换(Liskov Substitution)
子类能替换父类且不破坏行为——继承别破坏父类的约定。
java
// ❌ 坏:正方形继承长方形,破坏了"宽高独立"的约定
class Rectangle {
protected int w, h;
void setW(int w) { this.w = w; }
void setH(int h) { this.h = h; }
int area() { return w * h; }
}
class Square extends Rectangle {
void setW(int w) { this.w = w; this.h = w; } // 改了语义!
void setH(int h) { this.h = h; this.w = h; }
}
// 用父类接口操作时:r.setW(5); r.setH(4); r.area() 对 Square 变成 16(应 20)——行为被破坏
// ✅ 好:正方形不继承长方形(继承"形状"接口),各自实现
interface Shape { int area(); }
class Rect implements Shape { /* w*h */ }
class Sq implements Shape { /* side*side */ }一句话记住:"子类别比父类更弱"——继承是"is-a"关系,子类必须能完整替代父类。
四、I:接口隔离(Interface Segregation)
接口别太大,拆成小接口——不要强迫实现方实现用不到的方法。
java
// ❌ 坏:胖接口,飞鸟要实现 swim(),鱼要实现 fly()
interface Animal {
void eat();
void fly(); // 鱼:??
void swim(); // 鸟:??
}
class Bird implements Animal { /* swim() 只能空实现 */ }
// ✅ 好:拆成小接口,按能力实现
interface Eater { void eat(); }
interface Flyer { void fly(); }
interface Swimmer { void swim(); }
class Bird implements Eater, Flyer { } // 只实现会用的
class Fish implements Eater, Swimmer { }一句话记住:"胖接口拆成小接口"——客户端只依赖它用的那部分。
五、D:依赖倒置(Dependency Inversion)
依赖抽象,不依赖实现——接口定义在上层(核心),实现放下层。
java
// ❌ 坏:Service 直接 new 具体 DB 实现,换数据库要改业务代码
class UserService {
private MysqlUserDao dao = new MysqlUserDao(); // 依赖具体类
}
// ✅ 好:依赖接口,具体实现从外部注入(构造器)
interface UserRepository {
User findByName(String name);
}
class MysqlUserRepository implements UserRepository { }
class UserService {
private final UserRepository repo; // 依赖抽象
UserService(UserRepository repo) { this.repo = repo; } // 构造器注入
}
// 换数据库:new UserService(new MongoUserRepository()) —— 业务代码零改动一句话记住:"依赖抽象不依赖实现"——这正是《DDD 分层架构》洋葱图里"领域层不依赖任何框架和数据库"的实现机制(Repository 接口在领域层、实现在基础设施层)。
六、五个原则怎么配合
| 原则 | 防什么坏味道 | 和谁配合 |
|---|---|---|
| S 单一职责 | 上帝类(什么都会) | 拆分 + 接口隔离 |
| O 开闭原则 | 改老代码出 bug | 策略模式/模板方法 |
| L 里氏替换 | 继承破坏行为 | 多用组合少用继承 |
| I 接口隔离 | 胖接口、空实现 | 接口按能力拆分 |
| D 依赖倒置 | 核心依赖具体实现 | DI/IoC + DDD 分层 |
实际使用:设计新类时逐条过一遍(职责单一吗?扩展要改老代码吗?继承安全吗?接口太大吗?依赖抽象吗?)——五个全过,代码通常就差不了。不用刻意追求完美,过度拆分也是坏味道(见《编程思想》栏目其他文章)。
小结
- S:一个类一个职责(一个理由去改)
- O:扩展开放、修改关闭(加功能不改老代码)
- L:子类可替换父类(继承别破坏约定)
- I:接口隔离(胖接口拆小)
- D:依赖倒置(依赖抽象不依赖实现——DDD 洋葱图机制)
