SOLID 原则
SOLID 是面向对象设计的五大基本原则,帮助编写可维护、可扩展的代码。
一、SOLID 概览
| 原则 | 名称 | 核心思想 |
|---|---|---|
| S | 单一职责原则 (SRP) | 一个类只负责一件事 |
| O | 开闭原则 (OCP) | 对扩展开放,对修改关闭 |
| L | 里氏替换原则 (LSP) | 子类可以替换父类 |
| I | 接口隔离原则 (ISP) | 接口要小而专一 |
| D | 依赖倒置原则 (DIP) | 依赖抽象,不依赖具体 |
二、单一职责原则 (SRP)
Single Responsibility Principle
定义:一个类应该只有一个引起它变化的原因。
违反 SRP
// 一个类负责太多事情
public class UserService {
public void register(User user) { }
public void sendEmail(User user) { } // 邮件发送
public void generateReport() { } // 报告生成
public void saveToDatabase(User user) { } // 数据库操作
}遵循 SRP
// 用户服务:只负责用户相关
public class UserService {
public void register(User user) { }
}
// 邮件服务:只负责邮件发送
public class EmailService {
public void sendEmail(User user) { }
}
// 报告服务:只负责报告生成
public class ReportService {
public void generateReport() { }
}
// 数据访问:只负责数据库操作
public class UserRepository {
public void save(User user) { }
}判断标准
如果类有多个变化原因 → 违反 SRP
例:UserService 变化原因:
1. 用户注册逻辑变化
2. 邮件格式变化
3. 报告格式变化
→ 应该拆分三、开闭原则 (OCP)
Open/Closed Principle
定义:软件实体应该对扩展开放,对修改关闭。
违反 OCP
// 每次新增形状都要修改 AreaCalculator
public class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).getRadius() * ((Circle) shape).getRadius();
} else if (shape instanceof Rectangle) {
return ((Rectangle) shape).getWidth() * ((Rectangle) shape).getHeight();
}
// 新增形状需要修改这里
return 0;
}
}遵循 OCP
// 抽象
public interface Shape {
double calculateArea();
}
// 具体实现
public class Circle implements Shape {
private double radius;
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width;
private double height;
@Override
public double calculateArea() {
return width * height;
}
}
// 计算器无需修改
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
// 新增形状无需修改 AreaCalculator
public class Triangle implements Shape {
@Override
public double calculateArea() {
// 三角形面积计算
}
}关键技巧
- 使用抽象和多态
- 策略模式
- 工厂模式
四、里氏替换原则 (LSP)
Liskov Substitution Principle
定义:子类对象必须能够替换掉所有父类对象,而不影响程序正确性。
违反 LSP
// 正方形继承长方形
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 int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 强制正方形
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // 强制正方形
}
}
// 问题:用 Square 替换 Rectangle 会改变行为
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// 对于 Rectangle,面积应该是 20
// 对于 Square,面积是 16(违反预期)
}遵循 LSP
// 抽象出共同的父类
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width;
private int height;
@Override
public int getArea() { return width * height; }
}
public class Square extends Shape {
private int side;
@Override
public int getArea() { return side * side; }
}LSP 检查
能否用子类替换父类而不影响程序行为?
是 → 符合 LSP
否 → 违反 LSP五、接口隔离原则 (ISP)
Interface Segregation Principle
定义:客户端不应该依赖它不需要的接口。
违反 ISP
// 臃肿的接口
public interface Worker {
void work();
void eat();
void sleep();
}
// 机器人不需要吃和睡
public class Robot implements Worker {
@Override
public void work() { /* 工作 */ }
@Override
public void eat() {
throw new UnsupportedOperationException(); // 被迫实现不需要的方法
}
@Override
public void sleep() {
throw new UnsupportedOperationException(); // 被迫实现不需要的方法
}
}遵循 ISP
// 拆分为小接口
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// 人类实现所有接口
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() { /* 工作 */ }
@Override
public void eat() { /* 吃饭 */ }
@Override
public void sleep() { /* 睡觉 */ }
}
// 机器人只实现需要的接口
public class Robot implements Workable {
@Override
public void work() { /* 工作 */ }
}接口设计原则
接口要小而专一
一个接口只服务于一个客户端
避免"胖"接口六、依赖倒置原则 (DIP)
Dependency Inversion Principle
定义:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
违反 DIP
// 高层模块直接依赖低层模块
public class UserService {
private MySQLDatabase database = new MySQLDatabase(); // 直接依赖具体实现
public void save(User user) {
database.save(user);
}
}
// 切换数据库需要修改 UserService遵循 DIP
// 抽象
public interface Database {
void save(User user);
}
// 低层模块
public class MySQLDatabase implements Database {
@Override
public void save(User user) {
// MySQL 保存逻辑
}
}
public class PostgreSQLDatabase implements Database {
@Override
public void save(User user) {
// PostgreSQL 保存逻辑
}
}
// 高层模块依赖抽象
public class UserService {
private Database database;
// 依赖注入
public UserService(Database database) {
this.database = database;
}
public void save(User user) {
database.save(user);
}
}
// 使用
UserService service = new UserService(new MySQLDatabase());
// 或
UserService service = new UserService(new PostgreSQLDatabase());Spring 依赖注入
@Service
public class UserService {
private final UserRepository repository;
// 构造器注入(推荐)
public UserService(UserRepository repository) {
this.repository = repository;
}
}
// 或使用 @Autowired(字段注入,不推荐)
@Service
public class UserService {
@Autowired
private UserRepository repository;
}七、原则关系图
┌─────────────────────────────────────────────────────────┐
│ SOLID 原则 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ SRP 单一职责 │──────→ 类的粒度 │
│ └─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────┐ │
│ │ OCP 开闭原则 │──────→ 扩展机制 │
│ └─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────┐ │
│ │ LSP 里氏替换 │──────→ 继承关系 │
│ └─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────┐ │
│ │ ISP 接口隔离 │──────→ 接口设计 │
│ └─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────┐ │
│ │ DIP 依赖倒置 │──────→ 依赖关系 │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘八、面试高频问题
Q1: 为什么 SRP 很重要?
- 降低类复杂度
- 提高可读性和可维护性
- 降低变更风险
Q2: 如何实现 OCP?
- 使用抽象和多态
- 使用设计模式(策略、工厂、装饰器)
- 使用接口定义行为
Q3: LSP 的核心是什么?
- 子类必须完全实现父类行为
- 子类可以有自己的扩展行为
- 子类不能破坏父类的行为约束
Q4: ISP 和 DIP 有什么关系?
- ISP 关注接口大小
- DIP 关注依赖方向
- 两者都强调抽象的重要性
Q5: Spring 如何体现 SOLID?
| 原则 | Spring 体现 |
|---|---|
| SRP | 每个 Bean 职责单一 |
| OCP | 通过接口扩展 |
| LSP | 接口实现可替换 |
| ISP | 接口细粒度设计 |
| DIP | 依赖注入 |
九、最佳实践
1. 代码审查清单
□ 类是否只有一个变化原因?
□ 是否可以通过扩展而非修改来添加功能?
□ 子类能否完全替换父类?
□ 接口是否足够小?
□ 是否依赖抽象而非具体实现?2. 重构技巧
违反 SRP → 拆分类
违反 OCP → 提取抽象
违反 LSP → 重新设计继承关系
违反 ISP → 拆分接口
违反 DIP → 引入抽象层3. 平衡原则
不要过度设计
根据实际需求权衡
迭代中逐步优化更新时间:2026年3月16日