第二章节:单一职责原则——一个类只做一件事
单一职责原则,英文叫 Single Responsibility Principle,简称 SRP。说白了就是:一个类,只应该有一个引起它变化的原因。
我第一次接触这个原则时,觉得太简单了。不就是「一个类只做一件事」嘛,谁不懂?但真正在项目中落地,才发现没那么容易。我见过太多所谓的「万能类」,一个类里塞了业务逻辑、数据校验、日志打印、甚至还有发送邮件的方法。嗯,这种类,我称之为「定时炸弹」。
为什么一个类只做一件事这么重要?
你想想看,如果一个类承担了多个职责,会有什么后果?
- 修改一个功能,可能影响其他功能——比如你改了数据校验的逻辑,结果发现发送邮件的代码也报错了。为什么?因为它们在同一个类里,耦合在一起。
- 类变得臃肿,难以理解和维护——一个类几千行代码,新同事接手时想死的心都有。
- 复用性差——你想在其他地方复用某个功能,但发现它和一堆不相关的代码绑在一起,根本拆不开。
我在项目中遇到过这样一个案例:一个订单处理类,里面包含了订单校验、库存扣减、支付处理、物流发货、短信通知……整整五个职责。每次需求变更,都要动这个类。改一次,测试回归一次,线上出问题一次。后来我实在受不了,花了三天时间把它拆成了五个独立的类。从那以后,这个模块的 bug 率直接降了 70%。
如何判断一个类是否违反了单一职责?
这里我分享几个我常用的判断标准:
| 判断标准 | 说明 |
|---|---|
| 类名是否模糊 | 如果类名像「OrderManager」「UtilHelper」这种,大概率职责不单一 |
| 方法是否过多 | 一个类超过 20 个方法,就要警惕了 |
| 修改原因是否多样 | 问自己:这个类会因为哪些原因被修改?超过一个,就该拆分 |
| 依赖是否复杂 | 如果这个类依赖了很多不相关的组件,说明它管得太宽了 |
业务拆分实战:从臃肿类到清晰架构
光说不练假把式。我们来看一个真实的业务场景。
假设有一个电商系统的订单处理流程。原始代码可能是这样的:
public class OrderService {
public void createOrder(Order order) {
// 1. 校验订单
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("订单金额不合法");
}
// 2. 扣减库存
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 3. 保存订单
orderDao.save(order);
// 4. 发送短信通知
smsService.send(order.getUserId(), "您的订单已创建");
// 5. 记录日志
logger.info("订单创建成功: " + order.getId());
}
}
这个类干了五件事。如果哪天短信服务挂了,整个订单创建流程都会受影响。这合理吗?不合理。
按照单一职责原则,我们应该拆分成这样:
// 只负责订单校验
public class OrderValidator {
public void validate(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("订单金额不合法");
}
// 其他校验逻辑...
}
}
// 只负责库存操作
public class InventoryManager {
public void deductStock(Long productId, int quantity) {
// 扣减库存逻辑
}
}
// 只负责订单持久化
public class OrderRepository {
public void save(Order order) {
// 保存订单到数据库
}
}
// 只负责通知
public class NotificationService {
public void sendOrderCreatedNotification(Long userId) {
// 发送短信或推送通知
}
}
// 只负责日志
public class OrderLogger {
public void logOrderCreated(Order order) {
// 记录日志
}
}
然后,原来的 OrderService 变成了一个协调者:
public class OrderService {
private OrderValidator validator;
private InventoryManager inventoryManager;
private OrderRepository orderRepository;
private NotificationService notificationService;
private OrderLogger orderLogger;
public void createOrder(Order order) {
validator.validate(order);
inventoryManager.deductStock(order.getProductId(), order.getQuantity());
orderRepository.save(order);
notificationService.sendOrderCreatedNotification(order.getUserId());
orderLogger.logOrderCreated(order);
}
}
核心变化:每个类只负责一件事。修改短信逻辑时,不会影响库存扣减。测试时,可以单独测试每个类。复用性也大大提升——比如其他模块也需要扣库存,直接注入 InventoryManager 就行。
重构遗留系统中的臃肿类
说到重构,我经验还算丰富。我曾经接手过一个遗留系统,里面有一个叫 ReportGenerator 的类,足足 3000 多行。它干了什么?数据查询、报表计算、格式转换、导出 Excel、发送邮件……你能想到的它都干了。
重构这种类,我建议分三步走:
- 先梳理职责——把类里的每个方法列出来,按功能分组。比如数据查询一组、报表计算一组、导出一组。
- 再提取接口——为每组职责定义一个接口。比如
DataFetcher、ReportCalculator、ReportExporter。 - 最后逐步替换——不要一次性改完。先让新类实现接口,然后在原类中委托调用。测试通过后,再彻底替换。
我的小技巧:重构时,我会先写单元测试。把原类的行为用测试固定下来,然后再动手拆。这样即使拆错了,也能通过测试快速发现问题。
避坑指南
我曾经踩过的坑:过度拆分。有一回我把一个类拆成了十几个小类,每个类就一两个方法。结果呢?代码量翻倍,调用关系复杂到我自己都看不懂。单一职责不是越细越好,而是「职责内聚,变化隔离」。一个类的方法如果总是同时被修改,那它们就应该放在一起。
另外,不要为了单一职责而牺牲性能。比如你有一个方法需要同时更新订单状态和发送通知,如果硬拆成两个类两次调用,可能会引入事务一致性问题。这时候,可以适当妥协——在同一个类里用两个方法,但确保它们各自独立。
总结
单一职责原则,说白了就是让每个类都「专注」一点。我个人的习惯是:写代码前先问自己「这个类会因为什么原因被修改?」如果答案超过一个,那就说明需要拆分。
记住,好的设计不是一蹴而就的。重构臃肿类时,慢慢来,一步步拆。你会发现,代码越来越清晰,维护越来越轻松。