第二章节:单一职责原则——一个类只做一件事

单一职责原则,英文叫 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、发送邮件……你能想到的它都干了。

重构这种类,我建议分三步走:

  1. 先梳理职责——把类里的每个方法列出来,按功能分组。比如数据查询一组、报表计算一组、导出一组。
  2. 再提取接口——为每组职责定义一个接口。比如 DataFetcherReportCalculatorReportExporter
  3. 最后逐步替换——不要一次性改完。先让新类实现接口,然后在原类中委托调用。测试通过后,再彻底替换。

我的小技巧:重构时,我会先写单元测试。把原类的行为用测试固定下来,然后再动手拆。这样即使拆错了,也能通过测试快速发现问题。

避坑指南

我曾经踩过的坑:过度拆分。有一回我把一个类拆成了十几个小类,每个类就一两个方法。结果呢?代码量翻倍,调用关系复杂到我自己都看不懂。单一职责不是越细越好,而是「职责内聚,变化隔离」。一个类的方法如果总是同时被修改,那它们就应该放在一起。

另外,不要为了单一职责而牺牲性能。比如你有一个方法需要同时更新订单状态和发送通知,如果硬拆成两个类两次调用,可能会引入事务一致性问题。这时候,可以适当妥协——在同一个类里用两个方法,但确保它们各自独立。

总结

单一职责原则,说白了就是让每个类都「专注」一点。我个人的习惯是:写代码前先问自己「这个类会因为什么原因被修改?」如果答案超过一个,那就说明需要拆分。

记住,好的设计不是一蹴而就的。重构臃肿类时,慢慢来,一步步拆。你会发现,代码越来越清晰,维护越来越轻松。