架构原则:关注点分离、耦合与内聚
这一课你将能做到
- 说出"高内聚、低耦合"在代码层面的具体表现,并给出判题平台示例
- 用关注点分离把 Controller、Service、Repository 拆干净,不再"大杂烩"
- 用 CK 度量(LCOM、CBO、RFC)判断一个类是不是"设计得有问题"
为什么要学
设计原则告诉你"怎么写好一个类",架构原则告诉你"类与类之间怎么组织"。判题平台里,一个 SubmissionController 应该只做 HTTP 解析,SubmissionService 只做业务规则,SubmissionRepository 只做数据存取。如果这三层混在一起,改个字段要动三处,改个规则要动五处,这就是"耦合高、内聚低"。
先建立图像
高内聚 + 低耦合(好) 低内聚 + 高耦合(坏)
───────────── ─────────────
SubmissionController GodService
↓ 只依赖接口 ↓ 直接依赖所有
SubmissionService SubmissionMapper
↓ 只依赖接口 UserMapper
SubmissionRepository JudgeClient
NotifyService
RedisTemplate
...
跟着做
步骤 1:关注点分离——三层职责清晰
@RestController
public class SubmissionController {
private final SubmissionService submissionService;
public SubmissionController(SubmissionService submissionService) {
this.submissionService = submissionService;
}
@PostMapping("/submissions")
public ResponseEntity<SubmissionDto> submit(@RequestBody SubmitRequest req) {
Submission submission = submissionService.submit(req.toDomain());
return ResponseEntity.ok(SubmissionDto.from(submission));
}
}
@Service
public class SubmissionService {
private final SubmissionRepository submissionRepository;
private final JudgeDispatcher judgeDispatcher;
public SubmissionService(SubmissionRepository repo, JudgeDispatcher dispatcher) {
this.submissionRepository = repo;
this.judgeDispatcher = dispatcher;
}
public Submission submit(Submission submission) {
validate(submission);
Submission saved = submissionRepository.save(submission);
judgeDispatcher.dispatch(saved);
return saved;
}
}
@Repository
public class MybatisSubmissionRepository implements SubmissionRepository {
private final SubmissionMapper mapper;
// 只做数据存取,没有业务规则
}
每层只做一件事:Controller 做 HTTP 解析,Service 做业务规则,Repository 做数据存取。
步骤 2:低耦合——依赖接口而非实现
// 坏:直接依赖具体实现
public class SubmissionService {
private final MybatisSubmissionMapper mapper; // 绑死 MyBatis
private final JavaJudgeEngine judgeEngine; // 绑死 Java 判题
}
// 好:依赖抽象接口
public class SubmissionService {
private final SubmissionRepository repository; // 接口
private final JudgeEngine judgeEngine; // 接口
}
Service 不知道数据存哪、判题用什么语言,只认契约。换实现不动 Service。
步骤 3:CK 度量——用数字说话
LCOM(方法内聚缺乏度):类中方法之间共享字段的程度。LCOM 高 = 方法各干各的 = 内聚低。
// 反例:LCOM 高——方法之间几乎不共享字段
public class GodService {
private SubmissionMapper submissionMapper;
private UserMapper userMapper;
private RedisTemplate redis;
private JudgeClient judgeClient;
public void submit() { /* 只用 submissionMapper */ }
public void banUser() { /* 只用 userMapper */ }
public void cache() { /* 只用 redis */ }
public void judge() { /* 只用 judgeClient */ }
}
每个方法只用自己的字段,类被拆成四个独立的小世界。LCOM 接近 4(最大值),内聚极低。
重构:按职责拆成 SubmissionService、UserService、CacheService、JudgeService,每个类的方法都围绕自己的字段。
步骤 4:CBO(对象间耦合度)与 RFC(类响应集)
CBO:一个类直接依赖的其他类数量。CBO 高 = 耦合高。
// 反例:CBO 高
public class ReportService {
private SubmissionMapper submissionMapper;
private UserMapper userMapper;
private LessonMapper lessonMapper;
private PracticeRecordMapper practiceRecordMapper;
private NotifyMapper notifyMapper;
private FileStorage fileStorage;
private ExcelGenerator excelGenerator;
}
ReportService 依赖 7 个类,改任何一个都可能影响它。
重构:把报表拆成 SubmissionReportService、UserReportService,每个只依赖自己需要的 Mapper。
RFC:类的方法 + 方法里调用的外部方法总数。RFC 高 = 类做了太多事,或者方法里调用太多外部服务。
讲清楚要点
- 关注点分离:每层只做一件事。Controller 不查库,Service 不拼 HTTP,Repository 不做业务判断。
- 高内聚:一个类的方法都围绕同一组字段和同一个业务目标。
- 低耦合:类之间通过接口依赖,不直接 new 实现,不暴露内部细节。
- CK 度量是体检指标:LCOM 高 → 拆类;CBO 高 → 减依赖;RFC 高 → 拆方法或减外部调用。
- 架构原则不是教条:CRUD 项目里一个 Service 依赖 3 个 Mapper 是正常的;核心域服务依赖 7 个就是危险信号。
你可能会懵的地方
- 三层架构会不会太死板? 对简单 CRUD 确实够用;复杂业务可以演化到六边形、DDD 分层。但"关注点分离"这个思想是普适的,不管哪层架构。
- 怎么判断一个类是不是"上帝类"?
看方法数和字段数:方法 > 20、字段 > 10、LCOM 高、CBO 高,就是上帝类。判题平台里
GodService同时管提交、用户、缓存、判题,就是典型。 - CK 度量在 Java 里怎么算? 有工具(如 CKJM、SonarQube)自动算。初学者先凭感觉:一个类的方法是否都围绕同一组字段?是否依赖太多其他类?然后再用工具验证。
- 接口隔离和关注点分离什么关系? 关注点分离是"层与层之间"的隔离,接口隔离是"类与类之间"的隔离。一个管架构分层,一个管类设计,目标都是降低耦合。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| Controller 直接调 Mapper | 层间穿越,业务规则散落 | Controller → Service → Repository |
| Service 依赖 7 个 Mapper | CBO 高,改一个影响一片 | 按业务域拆 Service |
| 方法之间不共享字段 | LCOM 高,内聚低 | 拆成多个高内聚的类 |
| 为了"复用"把工具类继承 Service | 继承引入不必要耦合 | 组合:工具类持有 Service 接口 |
| 所有类都在同一个包 | 包级耦合,无法模块隔离 | 按业务域分包:submission、user、judge |
卡点急救
- 不知道 Service 该拆几个:画个"字段-方法"矩阵,看哪些方法共享哪些字段,不共享的拆出去。
- 想减依赖但不知道减哪个:列出这个类所有的 import,问自己"这个 import 换成接口行不行?"行就换。
- 三层之间 DTO 转换太烦:用 MapStruct 或简单 record 转换,别手写 setter 链;转换逻辑放专门的 Mapper 层。
面试常问
- 怎么判断一个系统耦合高不高?
答:看 CBO 和 RFC。一个类直接依赖多少其他类?方法里调用了多少外部方法?依赖越多、调用越多,耦合越高。判题平台里
ReportService依赖 7 个 Mapper 就是高耦合信号。 - 高内聚低耦合的具体表现?
答:高内聚:一个类的方法都围绕同一组字段;低耦合:类之间通过接口依赖,改实现不影响调用方。判题平台里
SubmissionService依赖SubmissionRepository接口而不是 MyBatis Mapper,就是低耦合。 - 关注点分离在 Spring Boot 项目里怎么落地? 答:Controller 只做 HTTP 解析和响应组装;Service 只做业务规则;Repository 只做数据存取;DTO 用 record 或 MapStruct 转换;跨层调用通过接口,不直接 new。
自测三问
- 判题平台里
GodService同时管提交、用户、缓存、判题,它的 LCOM 和 CBO 是高还是低?怎么重构? SubmissionService依赖SubmissionMapper而不是SubmissionRepository接口,问题在哪?- 一个 Controller 里写了 50 行业务逻辑,违反了什么架构原则?应该怎么改?
小结一句话
关注点分离让每层只干一件事,高内聚让类的方法围绕同一目标,低耦合让类之间通过接口对话。
延伸
- 《架构整洁之道》Robert C. Martin,第 8-11 章
- 《设计模式》GoF,第 1 章(组合优于继承)
- CK 度量工具 CKJM
- 下一课:架构风格——分层、六边形、CQRS 与事件驱动。