设计模式六大原则(SOLID)
说到设计模式,我总想起刚入行那会儿。那时候写代码,能跑就行,哪管什么原则不原则。直到有一次,一个同事离职,我接手他的项目——好家伙,一个类三千行,改一个功能要翻遍十几个文件。那感觉,就像在雷区里跳舞。
后来我才明白,设计模式不是花架子。它的根基,就是这六大原则。说白了,它们是写代码的「交通规则」。遵守了,代码就清晰、好改、不容易崩。不遵守,早晚要还债。
SOLID 原则 是面向对象设计的五个基本原则,加上迪米特法则,构成了我们日常编码的「行为准则」。
1. 单一职责原则(SRP)
一个类,只做一件事。听起来简单吧?但我见过太多「万能类」了。
我记得有一次重构一个订单系统。原来的 OrderService 里,既有订单计算,又有数据库操作,还负责发邮件。改一个逻辑,三个地方都要测。我花了整整两天,把它拆成 OrderCalculator、OrderRepository、EmailNotifier。从那以后,改需求再也不用提心吊胆了。
判断标准:如果你写一个类的职责时,需要用「和」字连接,那就说明它可能违反了单一职责。
// ❌ 违反单一职责
public class OrderService {
public void createOrder(Order order) {
// 计算价格
// 保存到数据库
// 发送邮件通知
}
}
// ✅ 符合单一职责
public class OrderCalculator {
public double calculateTotal(Order order) { ... }
}
public class OrderRepository {
public void save(Order order) { ... }
}
public class EmailNotifier {
public void sendConfirmation(Order order) { ... }
}
2. 开闭原则(OCP)
对扩展开放,对修改关闭。这句话我背了无数遍,但真正理解是在一次支付模块重构中。
原来的代码,每加一种支付方式就要改 PaymentService 里的 if-else。后来我改成策略模式,新增支付方式只需要加一个新类,老代码一行不动。你想想看,这多省心。
// ❌ 违反开闭原则
public class PaymentService {
public void pay(String type) {
if (type.equals("alipay")) { ... }
else if (type.equals("wechat")) { ... }
// 加新支付方式就要改这里
}
}
// ✅ 符合开闭原则
public interface PaymentStrategy {
void pay(double amount);
}
public class AlipayStrategy implements PaymentStrategy { ... }
public class WechatStrategy implements PaymentStrategy { ... }
// 新增:public class ApplePayStrategy implements PaymentStrategy { ... }
3. 里氏替换原则(LSP)
子类必须能替换父类,而且程序行为不变。嗯,这里要注意,不是「长得像」就能继承。
我曾经见过一个经典的反例:Square 继承 Rectangle。正方形重写了宽高设置方法,结果导致父类的逻辑全乱了。说白了,继承不是用来「复用代码」的,而是用来表达「is-a」关系的。
避坑指南:如果你发现子类重写父类方法时,需要抛出新的异常,或者改变方法的语义,那很可能违反了里氏替换原则。
// ❌ 违反里氏替换
public class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = w;
height = w; // 改变了父类的行为!
}
}
// ✅ 符合里氏替换:使用抽象接口
public interface Shape {
int getArea();
}
public class Rectangle implements Shape { ... }
public class Square implements Shape { ... }
4. 接口隔离原则(ISP)
客户端不应该依赖它不需要的接口。说白了,接口要小而专,不要大而全。
我习惯把接口设计成「角色」而非「实体」。比如一个员工,他可能既是「程序员」又是「会议参与者」。那就拆成两个接口,而不是一个「员工接口」包含所有方法。
// ❌ 违反接口隔离
public interface Worker {
void code();
void attendMeeting();
void manageTeam();
}
// ✅ 符合接口隔离
public interface Coder {
void code();
}
public interface MeetingParticipant {
void attendMeeting();
}
public interface Manager {
void manageTeam();
}
5. 依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
这句话有点绕,我换个说法:写代码时,尽量面向接口编程,而不是面向具体实现。我在项目中经常用依赖注入来实现这一点。Spring 的 IoC 容器,说白了就是 DIP 的典型实践。
// ❌ 违反依赖倒置
public class NotificationService {
private EmailSender emailSender = new EmailSender(); // 依赖具体实现
}
// ✅ 符合依赖倒置
public class NotificationService {
private MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender; // 依赖抽象
}
}
public interface MessageSender {
void send(String message);
}
6. 迪米特法则(LoD)
一个对象应该对其他对象保持最少的了解。也叫「最少知识原则」。
我有个习惯:如果一个方法里出现了连续的点号调用(比如 a.getB().getC().doSomething()),我就会警觉。这通常意味着耦合太深了。
实战技巧:迪米特法则不是让你完全不调用其他对象的方法,而是让你只和「直接的朋友」通信。直接的朋友包括:当前对象本身、作为参数传入的对象、成员变量引用的对象、方法内创建的对象。
// ❌ 违反迪米特法则
public class Customer {
public void buyItem(Store store) {
store.getInventory().getItem("book").purchase(); // 和陌生人通信
}
}
// ✅ 符合迪米特法则
public class Customer {
public void buyItem(Store store) {
store.purchaseItem("book"); // 只和直接朋友通信
}
}
六大原则的关系
这六个原则不是孤立的。它们互相配合,共同构成了设计模式的基石。我画了一张图,帮你理清它们的关系:
我的实践建议
学了这么多原则,你可能会问:实际项目中怎么用?我个人的习惯是这样的:
- 写代码前:先想想这个类的职责是否单一。如果感觉「这个类好像要做很多事」,那就拆。
- 写代码时:多用接口和抽象类。面向接口编程,而不是面向实现。
- 写代码后:检查一下有没有违反里氏替换。子类能不能无缝替换父类?如果不能,说明继承关系有问题。
- 重构时:看看有没有「万能类」或「长方法」。有的话,按单一职责拆开。
注意:原则是指导,不是教条。有时候为了性能或简化设计,适当违反某个原则是可以接受的。但你要清楚自己在做什么,并且承担后果。
好了,这就是设计模式的六大原则。它们就像写代码的「内功心法」。招式可以变,但内功不能丢。希望你在实际项目中,能慢慢体会到它们的价值。