LI YAO'S BLOG

有趣的人生, 一半家长里短, 一半山川胡海

SOLID_软件设计原则与示例

SOLID 软件设计原则详解(定义 · 解释 · 示例)

SOLID 是面向对象设计(Object-Oriented Design)中最重要、最经典的一组设计原则,其核心目标是:

控制变化、降低耦合、提升可维护性与可扩展性。

SOLID 并不是语法规则,而是设计层面的指导思想,在 Spring Boot、DDD、微服务架构中被广泛应用。

1. S - 单一职责原则 (Single Responsibility Principle, SRP)

  • 定义:一个类应该只有一个理由去改变,即一个类应当只有一个职责。
  • 解释:当一个类承担了多个职责时,修改一个职责可能会影响到其他职责,导致代码变得难以维护和扩展。遵循单一职责原则可以让每个类关注于自己的一部分功能,使得系统更易于理解和修改。
  • 示例:
    • 不要让一个类同时负责数据库操作和业务逻辑。应该分成两个类,一个处理数据库操作,另一个处理业务逻辑。

2. O - 开放封闭原则 (Open/Closed Principle, OCP)

  • 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
  • 解释:意味着在不修改现有代码的情况下,我们可以通过增加新功能来扩展系统的功能。这通过使用抽象和接口来实现,使得添加新功能时不需要修改已有的代码。
  • 示例:
    • 通过继承或者接口实现扩展,而不是直接修改已有的类。例如,新增一个支付方式时,不修改现有的支付类,而是通过实现相同的接口来添加新的支付方式。

3. L - 里氏替换原则 (Liskov Substitution Principle, LSP)

  • 定义:子类型必须能够替换掉它们的基类型,并且程序的行为不应该发生改变。
  • 解释:如果一个子类能够在任何使用父类的地方替代父类并且行为依然正常,那么子类就是符合里氏替换原则的。这要求子类必须遵循父类的契约,不能改变父类的期望行为。
  • 示例:
    • 假设有一个“鸟”的父类,和“鸽子”和“企鹅”作为子类。如果你将“企鹅”替换为“鸟”,而“企鹅”没有飞行能力,这将违反里氏替换原则。企鹅类不应该继承鸟类,应该有一个更合适的基类。

4. I - 接口隔离原则 (Interface Segregation Principle, ISP)

  • 定义:不应该强迫一个类去实现它不需要的接口。
  • 解释:接口应该是细粒度的,每个接口应该只包含类所需要的功能,避免设计出庞大且难以实现的接口。如果一个类只需要接口的一部分功能,应该拆分接口,避免接口污染。
  • 示例:
    • 假设你有一个打印机接口 Printer,它有打印、扫描、传真等方法。如果某些设备只需要打印功能,那它不应该强迫去实现扫描和传真功能。应当拆分成多个小接口,如 Printer, Scanner, Fax。

5. D - 依赖倒置原则 (Dependency Inversion Principle, DIP)

  • 定义:高层模块不应依赖低层模块,二者应该依赖于抽象;抽象不应依赖于细节,细节应依赖于抽象。
  • 解释:这一原则旨在通过依赖注入和抽象层来降低代码之间的耦合度。通过使高层模块依赖于抽象,而非低层实现,可以使系统更加灵活和易于维护。
  • 示例:
    • 比如你有一个数据库操作类,它直接依赖于具体的数据库实现(例如 MySQL)。根据依赖倒置原则,应该依赖于数据库操作的抽象接口(如 Database 接口),而不是具体的实现。这样可以轻松替换数据库实现而不影响系统的其他部分。

一、SRP —— 单一职责原则

1️⃣ 定义(Definition)

一个类应该只有一个引起它变化的原因。

也可以理解为:

一个类只负责一件事情,并且把这件事情做好。


2️⃣ 解释(Explanation)

如果一个类承担了多个职责,那么:

  • 修改某一个职责时,可能影响其他职责
  • 类会变得臃肿、难以理解
  • 测试和维护成本显著上升

单一职责原则的本质是:

把“变化原因”隔离开来。

在实际工程中:

  • 业务规则变化
  • 数据存储方式变化
  • 外部系统交互方式变化

这些都应该由不同的类来承担。


3️⃣ 示例(Example)

❌ 反例:一个类承担多个职责

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserService {

public void register(String username) {
// 业务逻辑
System.out.println("register user: " + username);

// 数据库存储
System.out.println("insert into user table");

// 发送邮件
System.out.println("send register email");
}
}

问题分析:

  • 业务逻辑变化 → 改
  • 数据库变化 → 改
  • 通知方式变化 → 改
    👉 至少 3 个变化原因

✅ 正例:按职责拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;

public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}

public void register(String username) {
userRepository.save(username);
emailService.sendRegisterMail(username);
}
}
1
2
3
4
5
public class UserRepository {
public void save(String username) {
System.out.println("insert into user table");
}
}
1
2
3
4
5
public class EmailService {
public void sendRegisterMail(String username) {
System.out.println("send register email");
}
}

✔ 每个类只有一个清晰的职责


二、OCP —— 开放封闭原则

1️⃣ 定义(Definition)

软件实体(类、模块、函数)应该对扩展开放,对修改封闭。


2️⃣ 解释(Explanation)

开放封闭原则强调:

  • 新增功能 → 通过扩展实现
  • 已有代码 → 尽量不修改

原因在于:

  • 已上线代码往往经过充分测试
  • 修改老代码容易引入不可预期的 Bug

实现 OCP 的核心手段:

抽象(接口 / 抽象类) + 多态


3️⃣ 示例(Example)

❌ 反例:频繁修改已有代码

1
2
3
4
5
6
7
8
9
10
public class PaymentService {

public void pay(String type) {
if ("ALIPAY".equals(type)) {
System.out.println("use alipay");
} else if ("WECHAT".equals(type)) {
System.out.println("use wechat pay");
}
}
}

问题分析:

  • 每新增一种支付方式都要修改 if-else
  • 违反“对修改封闭”

✅ 正例:基于抽象扩展

1
2
3
public interface Payment {
void pay();
}
1
2
3
4
5
public class AliPay implements Payment {
public void pay() {
System.out.println("use alipay");
}
}
1
2
3
4
5
public class WeChatPay implements Payment {
public void pay() {
System.out.println("use wechat pay");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class PaymentService {
private final Payment payment;

public PaymentService(Payment payment) {
this.payment = payment;
}

public void pay() {
payment.pay();
}
}

✔ 新增功能只需要新增实现类


三、LSP —— 里氏替换原则

1️⃣ 定义(Definition)

子类必须能够替换其父类,并且不影响程序的正确性。


2️⃣ 解释(Explanation)

里氏替换原则关注的是:

继承关系是否符合业务语义。

如果:

  • 父类能做的事
  • 子类却不能做

那么这种继承关系就是不合理的。

LSP 的核心思想是:

继承不是为了复用代码,而是为了表达“is-a”关系。


3️⃣ 示例(Example)

❌ 反例:错误的继承建模

1
2
3
4
5
public class Bird {
public void fly() {
System.out.println("bird can fly");
}
}
1
2
3
4
5
6
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("penguin can't fly");
}
}

问题分析:

  • 父类假设所有 Bird 都能飞
  • 子类破坏了这一假设

✅ 正例:合理的抽象层次

1
2
public abstract class Bird {
}
1
2
3
public interface Flyable {
void fly();
}
1
2
3
4
5
public class Sparrow extends Bird implements Flyable {
public void fly() {
System.out.println("sparrow flying");
}
}
1
2
public class Penguin extends Bird {
}

✔ 子类可以安全替换父类


四、ISP —— 接口隔离原则

1️⃣ 定义(Definition)

不应该强迫一个类实现它不需要的方法。


2️⃣ 解释(Explanation)

接口隔离原则强调:

  • 接口应该小而精
  • 使用方只依赖它真正需要的接口

过大的接口会导致:

  • 实现类臃肿
  • 修改接口影响范围极大

3️⃣ 示例(Example)

❌ 反例:接口过于臃肿

1
2
3
4
5
public interface Machine {
void print();
void scan();
void fax();
}
1
2
3
4
5
6
7
8
9
10
11
public class SimplePrinter implements Machine {
public void print() {}

public void scan() {
throw new UnsupportedOperationException();
}

public void fax() {
throw new UnsupportedOperationException();
}
}

✅ 正例:接口职责单一

1
2
3
public interface Printer {
void print();
}
1
2
3
public interface Scanner {
void scan();
}
1
2
3
4
5
public class SimplePrinter implements Printer {
public void print() {
System.out.println("print only");
}
}

✔ 实现类只关心自己需要的能力


五、DIP —— 依赖倒置原则

1️⃣ 定义(Definition)

高层模块不应该依赖低层模块,二者都应该依赖抽象。


2️⃣ 解释(Explanation)

依赖倒置原则的目标是:

降低模块之间的耦合度。

如果高层业务直接依赖具体实现:

  • 实现一旦变化
  • 业务代码就必须修改

通过引入抽象(接口):

  • 高层模块依赖抽象
  • 低层模块实现抽象

这也是 Spring IOC / DI 的理论基础。


3️⃣ 示例(Example)

❌ 反例:高层依赖具体实现

1
2
3
4
5
6
7
public class OrderService {
private final MySqlOrderRepository repository = new MySqlOrderRepository();

public void save() {
repository.save();
}
}

✅ 正例:依赖抽象 + 注入

1
2
3
public interface OrderRepository {
void save();
}
1
2
3
4
5
public class MySqlOrderRepository implements OrderRepository {
public void save() {
System.out.println("save to mysql");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class OrderService {
private final OrderRepository repository;

public OrderService(OrderRepository repository) {
this.repository = repository;
}

public void save() {
repository.save();
}
}

✔ 高层模块与具体实现彻底解耦


总结

原则 解决的核心问题
SRP 类职责过多、修改风险高
OCP 新功能频繁修改老代码
LSP 继承语义不正确
ISP 接口臃肿、实现困难
DIP 模块强耦合、不可替换

SOLID 的本质不是“形式上的接口设计”,而是:让系统在变化中保持稳定。

赞赏一杯咖啡