单一职责原则:一个类只做一件事
各位同学,今天我们来聊聊设计模式里最基础、也最容易被忽视的一条原则——单一职责原则。
说白了,就是一句话:一个类,只应该有一个引起它变化的原因。
听起来很简单对吧?但我见过太多项目,一个类动辄上千行,什么逻辑都往里塞。嗯,我早期也犯过这个错。有一次接手一个老系统,有个叫 OrderService 的类,里面既有订单校验、又有库存扣减、还有发送邮件……改一个功能,牵一发动全身,测试要跑半天。那滋味,真不好受。
什么是单一职责?
单一职责原则(Single Responsibility Principle,SRP)是 SOLID 原则的第一条。它强调:一个类应该只有一个职责,并且这个职责应该被完整地封装在类中。
你想想看,如果一个类承担了多个职责,那么当其中一个职责发生变化时,就可能影响到其他职责。这就好比一个厨师既要炒菜、又要收银、还要打扫卫生——任何一个环节出问题,整个餐厅都会乱套。
核心要点:
- 每个类只负责一个功能领域
- 类的变化原因只有一个
- 高内聚:类内部紧密相关
- 低耦合:类之间依赖最小化
高内聚与低耦合
这两个词,你肯定听过无数遍了。但我还是想用自己的理解给你讲透。
高内聚,指的是一个类内部的方法和数据,都围绕同一个目标服务。比如一个 UserValidator 类,所有方法都是关于用户数据校验的——校验邮箱、校验手机号、校验密码强度。这些方法之间关系紧密,这就是高内聚。
低耦合,指的是类与类之间的依赖关系尽量简单。一个类不应该知道太多其他类的内部细节。我习惯用「接口隔离」和「依赖注入」来实现低耦合,后面实战中你会看到。
我的经验:
判断一个类是否职责过重,有个简单方法:试着用一句话描述这个类「做了什么」。如果这句话里出现了「和」、「并且」、「同时」这些词,那大概率是职责过多了。
违反 SRP 的典型症状
我在代码评审时,经常看到这些「坏味道」:
- 类名带「And」或「Util」——比如
UserAndOrderUtil,一看就是大杂烩 - 一个类有多个不相关的字段——比如用户类里混入了订单状态
- 一个方法做了多件事——比如既查询数据库,又发通知,还写日志
- 修改一个功能需要改多个地方——这是最直接的信号
避坑指南:
我曾经在一个项目中,把「用户注册」和「用户登录」放在同一个 Service 里。结果后来需求变了,注册要加短信验证,登录要加图形验证码。两个逻辑纠缠在一起,改得我头皮发麻。从那以后,我坚持一个 Service 只做一件事。
实战:重构 UserService
好,理论说完了,我们来看一个真实的例子。
假设你接手了一个老项目,里面有这样一个 UserService:
public class UserService {
public void register(String username, String password, String email) {
// 1. 校验参数
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (password == null || password.length() < 6) {
throw new IllegalArgumentException("密码长度不能少于6位");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
// 2. 保存用户
User user = new User(username, password, email);
// 假设有数据库操作
saveToDatabase(user);
// 3. 发送欢迎邮件
sendWelcomeEmail(email);
// 4. 记录日志
log("用户注册成功: " + username);
}
private void saveToDatabase(User user) {
// 数据库操作
}
private void sendWelcomeEmail(String email) {
// 发送邮件
}
private void log(String message) {
// 写日志
}
}
这个类有什么问题?
问题大了去了。它至少承担了四个职责:参数校验、数据持久化、邮件发送、日志记录。任何一个职责变化,都要修改这个类。而且,这些逻辑无法复用——比如其他地方也要校验邮箱,还得再写一遍。
我们按照 SRP 来重构。先画出职责拆分图:
重构后的代码,我们拆成四个独立的类:
// 1. 参数校验类
public class UserValidator {
public void validate(String username, String password, String email) {
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (password == null || password.length() < 6) {
throw new IllegalArgumentException("密码长度不能少于6位");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
}
}
// 2. 数据持久化类
public class UserRepository {
public void save(User user) {
// 数据库操作
}
}
// 3. 邮件服务类
public class EmailService {
public void sendWelcomeEmail(String email) {
// 发送邮件
}
}
// 4. 日志服务类
public class LoggerService {
public void log(String message) {
// 写日志
}
}
// 5. 组合类——只负责编排
public class UserRegistrationService {
private UserValidator validator;
private UserRepository repository;
private EmailService emailService;
private LoggerService logger;
public UserRegistrationService(
UserValidator validator,
UserRepository repository,
EmailService emailService,
LoggerService logger) {
this.validator = validator;
this.repository = repository;
this.emailService = emailService;
this.logger = logger;
}
public void register(String username, String password, String email) {
validator.validate(username, password, email);
User user = new User(username, password, email);
repository.save(user);
emailService.sendWelcomeEmail(email);
logger.log("用户注册成功: " + username);
}
}
重构后的好处:
- 可测试性提升:每个类都可以单独写单元测试
- 可复用性提升:
UserValidator可以在登录、修改密码等场景复用 - 可维护性提升:改邮件逻辑只动
EmailService,不会影响其他代码 - 扩展性提升:想加短信通知?加个
SmsService就行,不用改现有类
什么时候可以「适度」违反?
你可能会问:是不是所有类都必须严格遵循 SRP?
我的回答是:原则是指导,不是教条。
在以下场景,我偶尔会「适度」放宽:
| 场景 | 说明 |
|---|---|
| 工具类 | 比如 StringUtils,虽然包含多个字符串操作方法,但都属于「字符串处理」这个大类 |
| 简单 CRUD | 如果业务逻辑极其简单,强行拆分反而增加复杂度 |
| 性能敏感 | 某些高频调用场景,过度拆分可能带来性能损耗 |
我的建议:
先按 SRP 写,等代码稳定后,再通过性能分析工具决定是否需要合并。不要一开始就为了「省事」而违反原则——省事的代价,往往是后期维护的噩梦。
总结
单一职责原则,说白了就是让每个类「各司其职」。它不是什么高深的理论,而是写代码时的一种习惯。
我个人的习惯是:写一个新类之前,先问自己三个问题:
- 这个类到底要做什么?
- 它的职责是否单一?
- 如果需求变了,改这个类会不会影响其他功能?
如果第三个问题的答案是「会」,那就说明职责没分清楚,需要重新设计。
记住:好的代码不是写出来的,是「拆」出来的。把大问题拆成小问题,把大职责拆成小职责,代码自然就优雅了。