在Java项目迭代过程中,随着业务需求不断增加,很容易出现一个类同时处理用户校验、数据持久化、日志打印、业务计算等多种逻辑的情况,这类复杂类不仅代码行数动辄上千,修改一处逻辑还可能引发其他功能异常。单一职责原则要求一个类只负责一项职责,我们可以通过这个原则将复杂类拆分为高内聚的细粒度业务单元,让代码结构更清晰。

单一职责原则的核心定义
单一职责原则(Single Responsibility Principle,SRP)是SOLID五大设计原则中的第一个,它的核心定义是:一个类应该只有一个引起它变化的原因。也就是说,一个类只负责一项具体的职责,如果它承担了多项职责,那么当其中一项职责发生变化时,就可能影响其他职责的正常运作。
判断一个类是否符合单一职责原则,核心看这个类是否存在多个不同的变更场景。比如一个用户服务类,如果既负责用户信息的校验、又负责用户数据的入库、还负责用户操作日志的记录,那么用户信息规则调整、数据库表结构变更、日志格式修改都会引发这个类的修改,显然它承担了过多职责。
复杂类的职责识别方法
在拆分复杂类之前,我们需要先识别出类中包含的所有职责,常见的识别方式有两种:
- 看类中的方法分组:把功能相似、变更原因相同的方法归为一组,每一组对应一个独立的职责
- 看变更场景:假设不同的需求变更(比如校验规则改、存储方式换、日志逻辑变),分别会影响类中的哪些方法,受影响的方法集合就是一个职责边界
拆分复杂类的完整步骤
1. 原始复杂类示例
我们先看一个违反单一职责原则的复杂用户服务类,它同时处理了用户校验、数据持久化、日志记录三项职责:
public class UserService {
// 用户校验相关方法
public boolean checkUserName(String userName) {
if (userName == null || userName.length() < 6) {
return false;
}
return true;
}
public boolean checkUserAge(int age) {
if (age < 18 || age > 60) {
return false;
}
return true;
}
// 数据持久化相关方法
public void saveUser(String userName, int age) {
System.out.println("将用户" + userName + "年龄" + age + "存入数据库");
}
// 日志记录相关方法
public void logUserSave(String userName) {
System.out.println("用户" + userName + "执行了保存操作,时间:" + System.currentTimeMillis());
}
// 组合业务方法
public void processUserSave(String userName, int age) {
if (!checkUserName(userName)) {
System.out.println("用户名不合法");
return;
}
if (!checkUserAge(age)) {
System.out.println("年龄不合法");
return;
}
saveUser(userName, age);
logUserSave(userName);
}
}
2. 拆分职责,定义细粒度单元
根据上面的职责识别,我们把这个类拆分为三个高内聚的细粒度业务单元:
- 用户校验单元:只负责用户信息的合法性校验,仅当校验规则变化时才会修改
- 用户持久化单元:只负责用户数据的存储操作,仅当存储方式或表结构变化时才会修改
- 操作日志单元:只负责用户操作日志的记录,仅当日志格式或输出方式变化时才会修改
3. 拆分后的代码实现
首先定义用户校验类:
public class UserValidator {
public boolean checkUserName(String userName) {
if (userName == null || userName.length() < 6) {
return false;
}
return true;
}
public boolean checkUserAge(int age) {
if (age < 18 || age > 60) {
return false;
}
return true;
}
}
然后定义用户持久化类:
public class UserRepository {
public void saveUser(String userName, int age) {
System.out.println("将用户" + userName + "年龄" + age + "存入数据库");
}
}
接着定义操作日志类:
public class UserLogService {
public void logUserSave(String userName) {
System.out.println("用户" + userName + "执行了保存操作,时间:" + System.currentTimeMillis());
}
}
最后定义新的用户服务类,组合上面的细粒度单元完成业务:
public class NewUserService {
private UserValidator userValidator;
private UserRepository userRepository;
private UserLogService userLogService;
public NewUserService() {
this.userValidator = new UserValidator();
this.userRepository = new UserRepository();
this.userLogService = new UserLogService();
}
public void processUserSave(String userName, int age) {
if (!userValidator.checkUserName(userName)) {
System.out.println("用户名不合法");
return;
}
if (!userValidator.checkUserAge(age)) {
System.out.println("年龄不合法");
return;
}
userRepository.saveUser(userName, age);
userLogService.logUserSave(userName);
}
}
拆分前后的效果对比
我们可以通过下面的表格直观看到拆分前后的差异:
| 对比维度 | 拆分前复杂类 | 拆分后细粒度单元 |
|---|---|---|
| 代码行数 | 单个类40+行,后续还会持续增长 | 每个类10-20行,职责清晰 |
| 变更影响范围 | 任意职责变更都可能引发整个类的修改,风险高 | 仅对应职责的类需要修改,不影响其他单元 |
| 可复用性 | 校验、持久化逻辑和业务流程耦合,无法单独复用 | 校验、持久化、日志单元可以在其他业务场景中单独复用 |
| 可测试性 | 需要 mock 大量无关逻辑才能测试单个功能 | 每个单元可以独立编写单元测试,测试成本低 |
拆分时的注意事项
在实际拆分过程中,不需要过度追求极致的拆分,要结合项目的实际情况判断:
- 如果类的逻辑非常简单,即使有多个职责,但是变更频率极低,也可以暂时不拆分,避免过度设计
- 拆分的粒度要合理,不要把一个职责拆分成过多过碎的类,反而增加调用复杂度
- 拆分后可以通过依赖注入的方式组合各个细粒度单元,进一步提升代码的灵活性
通过单一职责原则拆分复杂类,本质是把大的问题拆成小的独立问题,每个小部分专注做好一件事,最终让整个系统的代码更易维护、更易扩展。