设计原则回顾:六大原则的实战心法
做架构设计这么多年,我越来越觉得——设计原则这东西,不是背出来的,是踩坑踩出来的。今天咱们把六大原则再过一遍,我不讲教科书上的定义,咱们聊聊我在项目中是怎么理解它们的。
一、单一职责原则(SRP)
说白了就是一个类只干一件事。听起来简单,但我在代码评审里见过太多「万能类」了。
核心思想:一个类应该只有一个引起它变化的原因。
举个例子,我以前接手过一个订单处理类,里面混了订单校验、库存扣减、物流通知、用户积分计算……好家伙,一个类干了四个人的活。后来需求一变,改物流逻辑的时候,不小心把积分计算搞崩了。
怎么判断是否违反? 你问自己一个问题:这个类改动的理由是不是超过一个?如果是,就该拆了。
// 违反SRP:一个类处理订单、库存、物流
public class OrderService {
public void processOrder(Order order) {
validateOrder(order);
deductStock(order);
notifyLogistics(order);
updateUserPoints(order);
}
}
// 符合SRP:每个类只负责一件事
public class OrderValidator { ... }
public class InventoryService { ... }
public class LogisticsNotifier { ... }
public class UserPointsService { ... }
我的习惯:写代码前先画个职责边界图,每个类只放一个核心职责。如果发现一个类里出现了「并且」「同时」这类词,就该拆了。
二、开闭原则(OCP)
对扩展开放,对修改关闭。这话听着抽象,其实核心就一句:加新功能的时候,尽量别改老代码。
我记得有一次做支付系统,一开始只支持微信支付。后来要加支付宝,如果直接在老代码里加if-else,那每次加新支付方式都得改一遍。后来我改成策略模式,每种支付方式一个类,新增支付方式只需要加新类,老代码动都不动。
// 违反OCP:每次加新支付方式都要改这里
public class PaymentService {
public void pay(String type) {
if ("wechat".equals(type)) { ... }
else if ("alipay".equals(type)) { ... }
}
}
// 符合OCP:扩展新支付方式只需加新类
public interface PaymentStrategy {
void pay(Order order);
}
public class WechatPay implements PaymentStrategy { ... }
public class AlipayPay implements PaymentStrategy { ... }
避坑指南:我曾经在一个项目里过度追求开闭原则,把简单逻辑硬拆成十几个接口和实现类,结果代码反而更难维护了。记住,开闭原则不是让你把所有东西都抽象化,而是要在「变化点」上做抽象。
三、里氏替换原则(LSP)
子类应该能替换父类,而且程序行为不变。说白了就是:你说是鸟,就得会飞;你说是鱼,就得会游。别搞个「企鹅」硬说它是鸟,结果飞不起来。
我在项目中见过一个经典的反例:有个矩形类,有宽和高。后来想复用代码,让正方形继承矩形。结果正方形的setWidth方法把高度也改了,导致所有依赖矩形的地方都出了问题。
// 违反LSP:正方形不能完全替代矩形
public class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // 强行保持正方形,但破坏了父类行为
}
}
// 正确做法:让矩形和正方形都继承自「形状」抽象类
我的判断方法:写继承前先问自己——子类能不能完全替代父类?如果不能,就别用继承,用组合。
四、依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
这句话有点绕,我换个说法:你的业务逻辑代码,不要直接依赖具体的数据库、具体的第三方服务。要依赖接口。这样换数据库、换服务的时候,业务代码不用动。
// 违反DIP:业务层直接依赖MySQL
public class UserService {
private MySQLUserRepository repo = new MySQLUserRepository();
}
// 符合DIP:业务层依赖接口
public class UserService {
private UserRepository repo; // 依赖抽象
public UserService(UserRepository repo) {
this.repo = repo; // 具体实现由外部注入
}
}
实战经验:我习惯在项目初期就把核心业务接口定义好,具体实现放到后面再写。这样即使底层技术方案没定,业务代码也能先写起来。
五、接口隔离原则(ISP)
客户端不应该依赖它不需要的接口。说白了就是:接口别设计得太胖,要瘦身。
我见过一个「万能接口」,里面塞了增删改查、导出报表、发送通知、生成日志……结果每个实现类都得实现一堆用不到的方法,要么空实现,要么抛异常。
// 违反ISP:接口太胖
public interface Worker {
void work();
void eat();
void sleep();
void manage();
}
// 符合ISP:拆成多个小接口
public interface Workable { void work(); }
public interface Eatable { void eat(); }
public interface Manageable { void manage(); }
我曾经踩过的坑:一开始觉得接口拆太细麻烦,就搞了个大接口。结果后来每个新需求都要改这个接口,所有实现类都得跟着改。那叫一个酸爽。现在我的原则是:接口方法不超过5个,超过就考虑拆分。
六、迪米特法则(LoD)
也叫最少知识原则。一个对象应该对其他对象有最少的了解。不要和陌生人说话。
举个例子,你让下属去办事,你只需要告诉他「去把这件事办了」,不需要知道他是怎么坐车、怎么找人、怎么签字的。代码里也一样,A对象调用B对象的方法,不要通过B再去拿C对象的方法。
// 违反迪米特法则:A直接操作了C
public class A {
public void doSomething(B b) {
C c = b.getC();
c.doSomething(); // A和C产生了耦合
}
}
// 符合迪米特法则:B封装好操作,A只和B打交道
public class A {
public void doSomething(B b) {
b.doSomething(); // A只依赖B
}
}
我的习惯:写代码时如果发现一个方法里出现了「点链」——比如 a.getB().getC().doSomething()——我就知道该重构了。
知识体系总览
下面这张图把六大原则的核心关系画出来了,方便你整体把握:
六大原则的实战关系
这些原则不是孤立的。我举个例子你就明白了:
假设你要设计一个订单处理系统。SRP告诉你订单校验、库存扣减、支付处理要分开。OCP告诉你加新支付方式时不要改老代码。LSP告诉你子类要能替代父类。DIP告诉你业务层依赖接口而不是具体实现。ISP告诉你支付接口别塞太多方法。LoD告诉你订单处理类不要直接操作第三方支付网关的内部对象。
你看,一个功能设计,六个原则全用上了。
| 原则 | 核心问题 | 一句话总结 |
|---|---|---|
| SRP | 一个类该做多少事? | 一个类只干一件事 |
| OCP | 如何应对需求变化? | 加功能不改老代码 |
| LSP | 继承关系是否合理? | 子类要能替代父类 |
| DIP | 依赖关系怎么设计? | 面向接口编程 |
| ISP | 接口该多大? | 接口要瘦身 |
| LoD | 对象间该知道多少? | 不和陌生人说话 |
最后说两句:设计原则是指导方针,不是法律条文。我见过太多人为了遵守原则而过度设计,反而把代码搞复杂了。记住,原则是为可维护性服务的,如果某个原则让你的代码更难维护,那一定是理解出了问题。
嗯,这六个原则,说白了就是让你写出来的代码好改、好扩展、好维护。下次写代码前,拿这六条过一遍,能帮你少走不少弯路。