引言
在程序员的日常学习中,设计模式 几乎是"必修课"。但相比之下,设计原则往往被忽视,很多人甚至听都没听过。
其实,设计原则才是设计模式的根基。如果不理解原则,只会照搬模式,往往就会出现"为了模式而模式"的情况,代码看起来用了模式,但本质依旧混乱。
因此,本文将重点介绍六大设计原则,并结合真实业务场景,说明如果违反这些原则可能带来的后果。对于设计模式还不太熟悉的同学,可以先阅读《设计模式》专栏,再回头理解原则,效果会更好。
1、单一职责原则(SRP)
定义: 一个类只负责一项职责,避免出现"万能类"。
常见反例(业务场景) : 在实际项目中,经常能看到这样的写法:
- 用户相关业务全部写在
UserService
里; - 订单相关业务全部写在
OrderService
里; - 商品相关业务全部写在
GoodsService
里。
时间一长,就会带来以下问题:
- 类体积庞大:单个类动辄几千行,IDE 打开卡顿,多人开发时冲突频发;
- 逻辑高度耦合:功能之间互相牵扯,一个小改动可能引发连锁问题;
- 循环依赖风险:不同"万能类"之间互相引用,Spring 虽能解决部分场景,但复杂时依然可能导致项目无法启动;
- 扩展性差:新增功能只能继续往这个大类里堆代码,维护和测试成本急剧上升。
优化方案 : 以用户服务为例,登录、资料更新、积分计算本质上是三种不同的职责。如果都堆在 UserService
中,会让逻辑混乱。更合理的做法是:
LoginService
:专门负责用户登录;UserProfileService
:专门负责用户资料维护;UserPointService
:专门负责用户积分逻辑。
其他模块同理,订单、商品业务也应按职责拆分。 一个实用的判断标准是:如果某个功能会被其他类频繁依赖,就应该单独抽取出来。这样类之间的依赖关系会更加清晰,尽量保持线性依赖,避免循环依赖。
小结 单一职责原则的核心价值是 解耦与清晰。把"万能类"拆分成小而专的类,既能提升代码可读性和可维护性,也能避免扩展时陷入"牵一发动全身"的困境。
2、开闭原则(OCP)
定义: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。换句话说,需求变化时尽量通过"增加代码"来实现,而不是"修改已有代码"。
常见反例(业务场景) : 在支付业务中,很多系统一开始写成这样:
csharp
if (payType.equals("WECHAT")) {
// 微信支付逻辑
} else if (payType.equals("ALIPAY")) {
// 支付宝支付逻辑
}
这种写法的隐患:
- 违背开闭原则 :每增加一种支付方式,就要修改
PayService
; - 风险高:修改老代码容易引入回归 bug;
- 可维护性差:if/else 会越来越多,代码臃肿难读。
优化方案 : 通过 策略模式 + 多态 进行解耦:
- 定义统一的
PayStrategy
接口; - 为微信、支付宝、Apple Pay、银联支付分别实现对应的策略类;
- 使用工厂或 Spring 的依赖注入,根据
payType
自动选择对应策略。
这样新增支付方式时,只需增加一个类,不需要改动已有的 PayService
,既符合开闭原则,又降低了风险。
推荐阅读《设计模式专栏(五):支付系统扩展与回调处理案例》,里面有完整实现示例。
小结 : 开闭原则的核心价值在于:通过扩展而非修改来适应变化。它让系统在需求不断变化时仍能保持稳定,减少回归风险,提升扩展性。
3、里氏替换原则(LSP)
定义: 子类对象必须能够替换父类对象,且程序逻辑不会因此出错。继承关系要符合"is-a"的逻辑,子类不能削弱或违背父类的约定。
常见反例(业务场景) : 假设我们在权限系统里定义了一个用户基类:
typescript
public class User {
public void viewPage(Page page) {
// 用户查看页面
}
}
现在新增一个 NormalUser
(普通用户),继承了 User
,但我们在实现时却这样写:
java
public class NormalUser extends User {
@Override
public void viewPage(Page page) {
throw new UnsupportedOperationException("普通用户不允许查看该页面");
}
}
问题来了:
- 按照语义,普通用户是用户(
NormalUser is a User
),应该具备用户的所有基本行为; - 但在子类里却禁止了
viewPage
的行为,导致替换成NormalUser
时业务逻辑崩溃; - 这就是典型的违反 里氏替换原则。
优化方案: 调整抽象设计:
- 把用户抽象为
AbstractUser
,定义最小公共能力; - 将"查看页面"权限交给 权限控制组件 判断,而不是写死在子类;
- 这样,
NormalUser
替换User
时不会破坏原有逻辑,而权限差异通过策略或配置解决。
例如:
java
public abstract class AbstractUser {
public abstract void access(Page page);
}
public class NormalUser extends AbstractUser {
@Override
public void access(Page page) {
// 普通用户查看页面
}
}
public class AdminUser extends AbstractUser {
@Override
public void access(Page page) {
// 管理员查看页面 + 管理员特权操作
}
}
权限差异通过 PermissionService
来做限制,而不是在继承关系里硬编码。
小结: 里氏替换原则提醒我们:
- 继承要合理,子类不能削弱父类功能;
- 语义必须一致 ,
NormalUser
是User
,就必须至少能做用户能做的事; - 权限、条件限制应交由 组合/策略 去实现,而不是破坏继承关系。
4、接口隔离原则(ISP)
定义: 不要让一个接口承担过多职责,接口应该小而精,按需设计,避免强迫实现无关方法。
常见反例(业务场景) : 在一个通知系统里,我们定义了一个大接口:
arduino
public interface NotificationService {
void sendEmail(String to, String content);
void sendSms(String to, String content);
void sendPush(String to, String content);
}
问题:
- 如果一个类只需要实现短信通知,就被迫同时实现
sendEmail
、sendPush
方法,哪怕方法体是空的,也破坏了接口的纯洁性; - 当新增一种通知方式(如站内信)时,所有实现类都要跟着修改。虽然 Java 8 之后接口可以增加默认方法,避免了强制实现,但问题依旧存在:接口方法越来越多,职责越来越混乱,接口本身变得臃肿;
- 接口变成了"胖接口",违背了接口隔离原则。
优化方案: 将接口拆分为更小的接口,按需实现:
arduino
public interface EmailNotification {
void sendEmail(String to, String content);
}
public interface SmsNotification {
void sendSms(String to, String content);
}
public interface PushNotification {
void sendPush(String to, String content);
}
这样:
- 只发短信的类只实现
SmsNotification
; - 只发推送的类只实现
PushNotification
; - 如果要同时支持多渠道,可以实现多个接口。
小结 : 接口隔离原则强调 精简接口,职责单一。
- 开发者只需实现与自身业务相关的接口,避免冗余方法;
- 当业务扩展时,不会牵连无关实现类;
- 从架构角度看,接口越小,系统的灵活性和可维护性就越高。
5、依赖倒置原则(DIP)
定义: 高层模块不应该依赖底层模块,二者都应该依赖于抽象。
常见反例(业务场景) : 假设多个业务类(如 UserService
、OrderService
、CouponService
)都直接依赖 SmsNotification
发送短信通知:
typescript
public class UserService {
private SmsNotification smsNotification = new SmsNotification();
public void registerUser(String userId) {
// 业务逻辑
smsNotification.send(userId, "注册成功");
}
}
问题:
- 如果后续改成 邮箱通知 或 站内信通知,这些服务类都要修改,耦合度极高。
- 系统缺乏灵活性,扩展新通知方式时改动面巨大。
优化方案: 引入抽象
- 定义
Notification
接口 - 各业务类依赖
Notification
接口,而不是具体实现 - 通过依赖注入(DI)传入不同实现类(短信、邮箱、站内信等)
typescript
public interface Notification {
void send(String userId, String message);
}
public class SmsNotification implements Notification {
@Override
public void send(String userId, String message) {
System.out.println("短信通知: " + message);
}
}
public class EmailNotification implements Notification {
@Override
public void send(String userId, String message) {
System.out.println("邮箱通知: " + message);
}
}
public class UserService {
private final Notification notification;
// 构造注入
public UserService(Notification notification) {
this.notification = notification;
}
public void registerUser(String userId) {
// 业务逻辑
notification.send(userId, "注册成功");
}
}
这样,如果未来新增 PushNotification ,只需要新增实现类,并在 Spring 配置里替换注入方式即可,UserService
无需修改。
小结 : 依赖倒置的核心价值是 面向接口编程,高层模块只关心抽象能力,不关心底层实现。这样系统更灵活、可扩展。
6、迪米特法则(LOD,最少知道原则)
定义: 一个对象应当尽量少了解其他对象的内部细节,只与"直接朋友"交互。
常见反例(业务场景) : 假设业务层直接操作通知发送的底层对象:
typescript
public class UserService {
private SmsNotification smsNotification;
public void registerUser(String userId) {
// 短信通知业务封装过于开放,为了"自由和扩展",调用方必须直接操作底层 Sender 和 Connection,
// 相当于让业务层自己实现了发短信的功能,耦合度高,扩展性差
smsNotification.getSender().getConnection().sendMessage(userId, "注册成功");
}
}
问题:
UserService
依赖了Sender
、Connection
等一堆内部对象。- 一旦底层
SmsNotification
内部结构发生变化(比如换成第三方 API),业务层也要跟着改。 - 调用链条过长,耦合严重。
优化方案 : 让 Notification
对外只暴露必要方法,隐藏内部实现细节:
typescript
public interface Notification {
void send(String userId, String message);
}
public class SmsNotification implements Notification {
private Sender sender;
@Override
public void send(String userId, String message) {
sender.getConnection().sendMessage(userId, message);
}
}
public class UserService {
private final Notification notification;
public UserService(Notification notification) {
this.notification = notification;
}
public void registerUser(String userId) {
// 业务逻辑
notification.send(userId, "注册成功");
}
}
这样:
UserService
只需要知道Notification
提供的send
方法。- 内部
Sender
、Connection
的变更对业务层透明。 - 系统耦合度降低,扩展性提升。
小结 : 迪米特法则的核心是 隐藏不必要的复杂性,让调用方只和直接的抽象交互,避免依赖过多的中间细节。
设计原则 vs 设计模式
1. 定义层面
- 设计原则(Design Principles) 是一种 思想和指导方针 ,告诉你"写代码应该遵循什么样的规则",更偏理念层,比如"一个类只做一件事"(单一职责)。
- 设计模式(Design Patterns) 是一种 具体的可复用解决方案 ,告诉你"遇到某类问题可以用什么样的代码结构去解决",更偏实现层,比如"用策略模式来扩展支付方式"。
2. 关注点不同
-
设计原则 → 注重架构质量和代码规范
- 更关注系统的可维护性、扩展性、解耦性。
- 偏"高层次",像指导思想。
- 举例:开闭原则告诉我们"要通过扩展而不是修改来应对需求变化"。
-
设计模式 → 注重具体问题的解决办法
- 更关注"怎么写代码"来实现这些原则。
- 偏"实践层",是原则的落地实现。
- 举例:策略模式就是开闭原则的落地方案之一。
3. 是否需要代码实现
- 设计原则 :本身不需要代码实现,它是代码背后的思想。比如 SRP(单一职责),你即使没有用设计模式,也能写出符合原则的代码。
- 设计模式 :一定需要代码结构的组织,比如类图、接口、继承、组合等,才能体现出模式。
4. 抽象 vs 具体
- 原则 → 偏抽象,更通用,不局限于某种语言或框架。
- 模式 → 偏具体,往往结合语言特性、编程习惯(比如 Java 常见的工厂模式、观察者模式)。
总结
本文介绍了六大设计原则(SRP、OCP、LSP、ISP、DIP、LOD),并结合实际业务场景说明了违反原则可能带来的问题以及优化方案。总结核心要点如下:
-
设计原则是代码背后的思想 它们指导我们如何组织代码、设计架构、降低耦合、提升可维护性和扩展性。
-
设计模式是原则的落地实现 模式提供了可复用的具体方案,让原则在代码中得以体现。理解原则可以让你更灵活地运用模式,而不是照搬。
-
遵循原则可以带来显著价值
- 代码更清晰、职责更单一
- 系统更易扩展,修改风险降低
- 团队协作更高效,减少冲突和误解
- 系统耦合度低,后期维护成本更可控
-
原则和模式相辅相成 在实际开发中,先理解设计原则,再选择合适的设计模式去实现,会让你的系统架构更加稳健、灵活、可持续。
最终目标不是死板地记住规则,而是养成以原则思考、用模式实现的能力。这样,你写出的每一行代码都更有价值,系统也更容易演进。