单一职责的判断标准是看类中每个public方法是否服务于同一业务概念;若方法变化原因不同(如sendEmail与generateReport),则违反该原则,应通过委托、接口抽象等方式安全拆分,并确保测试聚焦单一职责。

单一职责怎么判断类有没有“干太多事”
看一个类的每个 public 方法是否都在为同一个业务概念服务。比如 UserManager 里既有 saveUser()、sendEmail(),又夹着 generateReport(),那它大概率违反了单一职责——sendEmail() 和 generateReport() 的变化原因完全不同。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 每次新增一个
public方法前,先问:这个逻辑如果换一种通知方式(比如从邮件改成短信),或换一种报表格式(PDF → Excel),是否要改这个类?如果答案是“是”,就该拆。 - 把日志、校验、序列化等通用行为抽成独立类或注解,别让业务类自己
log.info()或new ObjectMapper()。 - IDE 中右键类 → “Find Usages”,观察调用方分布:如果
UserService被 Web 层、定时任务、消息监听器三处调用,但每处只用其中 1–2 个方法,说明它其实承担了多个角色。
重构时如何安全地拆分 UserManager
不要一上来就删方法,而是先隔离依赖、再迁移行为。重点不是“怎么拆”,而是“怎么不让调用方感知变化”。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 新建
EmailSender类,把原UserManager.sendEmail()搬过去,保留原方法作为委托:public void sendEmail(User user) { emailSender.send(user); }—— 这样调用方不用改,你却拿到了可测试、可替换的边界。 - 把数据库操作单独拎出
UserRepository,用接口定义(UserRepository),实现类叫JpaUserRepository。这样未来换成 Redis 或 Feign 调用,只换实现,不碰业务逻辑。 - 避免“拆着拆着变成一堆
xxxHelper”:Helper 类往往职责模糊,命名应体现能力,如PasswordEncoder、UserValidator,而不是UserHelper。
为什么 @Transactional 放错位置会让内聚失效
把 @Transactional 加在 UserManager.updateProfile() 上看似方便,但它会把数据一致性、重试逻辑、事务传播行为全部耦合进业务方法签名里,导致这个方法既要做业务判断,又要管底层资源控制。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 事务边界应由最外层协调者控制,比如 Controller 或 Service 编排层。业务类
UserProfileService只负责“更新头像URL”和“校验昵称长度”,不声明事务。 - 如果必须在 service 层启事务,用专门的编排类,比如
UserProfileUpdateOrchestrator,里面组合UserProfileService和NotificationService,并在其方法上加@Transactional。 - 注意 Spring 的代理限制:同类中方法 A 调用本类的带
@Transactional方法 B,事务不生效。这种写法本身就是高耦合信号,得拆。
测试写不下去?往往是职责没切干净
当你发现一个单元测试要 mock 数据库、邮件客户端、第三方 API 才能跑通 UserManager.testSaveAndNotify(),说明这个类已经同时承担了领域逻辑、基础设施适配、流程编排三重责任。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 每个被测类只依赖接口,且依赖不超过 3 个。超过就说明它在协调太多东西,该拆。
- 用构造函数注入所有依赖,拒绝
static工具类或ApplicationContext.getBean()—— 后者会让你根本没法在测试里替换行为。 - 测试命名暴露职责:不是
testUserManager(),而是shouldSendWelcomeEmailWhenUserIsCreated(),对应EmailSender类;shouldRejectNickNameWithSpecialChars()对应UserValidator。
真正难的不是拆类,而是识别哪些逻辑“看起来相关,实则变化原因不同”。比如用户注册时发短信和写操作日志,都发生在同一时间点,但短信通道可能下周就换供应商,而日志格式可能下季度才调整——它们不该待在同一个类里。










