分层架构与模块拆分
1. 这是什么
分层架构与模块拆分,是把系统按职责和边界组织起来的方法。
它的目标不是为了层数好看,而是为了让系统更清晰、更稳定、更容易演进。
一句话理解:
- 分层是在回答“不同职责放哪一层”
- 模块拆分是在回答“不同能力边界怎么分开”
2. 为什么重要
没有合理分层,业务逻辑、数据访问、接口处理会混在一起。
没有合理模块边界,系统一旦扩大就会变得:
- 难改
- 难测
- 难协作
很多“项目越写越乱”的根因,并不是技术栈不高级,而是:
- 职责边界失控
- 依赖方向失控
3. 先建立直觉:分层是为了隔离变化,不是为了堆目录
最容易误解的一点是:
- 看到
controller/service/repository三层,就以为已经完成了架构设计
其实真正重要的是:
- 哪类变化应该被隔离在哪一层
例如:
- HTTP 细节变化,不应该影响核心业务规则
- 数据库实现变化,不应该直接改动 Controller
- 一个模块内部变动,不应该轻易传染整个系统
所以分层和拆模块的核心不是形式,而是:
- 控制变化传播范围
4. 核心内容
4.1 Controller、Service、Repository 的职责
可以先用最经典的后端分层来建立直觉:
Controller
负责:
- 接收请求
- 参数校验与转换
- 返回统一响应
不应该负责:
- 复杂业务规则
- 直接拼 SQL
Service
负责:
- 业务流程编排
- 业务规则判断
- 事务边界控制
不应该负责:
- HTTP 细节
- 底层数据库访问细节
Repository
负责:
- 数据访问
- 对持久化细节做封装
不应该负责:
- 业务流程编排
- Web 层响应组装
4.2 模块职责边界
模块拆分最核心的不是“拆多少个”,而是:
- 一个模块是否围绕稳定职责聚合
更实用的判断方式是:
- 这个模块是否有清晰边界
- 这个模块内部变化是否主要由同一类原因触发
4.3 依赖方向控制
一个健康系统常见的依赖方向是:
- 上层依赖下层
- 接口层依赖应用层
- 应用层依赖领域或数据访问层
最危险的是:
- 反向依赖
- 横向乱依赖
- 模块循环依赖
因为一旦依赖方向失控,修改一个点就可能牵动整片代码。
4.4 领域模型与数据模型的区分
很多系统一开始会直接把数据库表结构对象当作整个业务模型。
这样虽然短期快,但中后期容易出问题。
更实用的理解是:
- 领域模型关注业务语义
- 数据模型关注存储结构
二者可以重合,但不要默认它们必须完全相同。
4.5 高内聚、低耦合怎么落地
这两个词很常见,但落地时最好转成具体判断:
- 一个模块内部的类是否围绕同一职责协作
- 一个模块是否暴露了尽量少、尽量稳定的接口
- 模块之间是否通过清晰契约交互,而不是互相穿透实现细节
5. 学习重点
这一章最重要的是掌握:
- 分层是为了隔离变化,不是为了形式完整
- 模块拆分要围绕职责边界,而不是机械按目录切
- 依赖方向必须可控
- Controller、Service、Repository 三层职责要清楚
- 模块边界一旦混乱,后续演进成本会快速上升
6. 常见问题
6.1 Controller 里写大量业务逻辑
这会让:
- 接口层承担业务复杂度
- 测试和复用都变差
6.2 Service 直接拼 SQL 或操作 HTTP 细节
这说明层次职责已经开始混杂。
6.3 模块之间循环依赖
循环依赖会让系统越来越难拆、越来越难替换实现。
7. 动手验证
这一节我用一个最小分层 demo,把“接口层 -> 业务层 -> 数据访问层”的职责边界直接跑出来。
7.1 准备一个可运行示例
新建文件 LayeredArchitectureDemo.java,内容如下:
java
public class LayeredArchitectureDemo {
static class UserEntity {
long id;
String name;
UserEntity(long id, String name) {
this.id = id;
this.name = name;
}
}
interface UserRepository {
UserEntity findById(long id);
}
static class MemoryUserRepository implements UserRepository {
@Override
public UserEntity findById(long id) {
System.out.println("repository-findById");
return new UserEntity(id, "user-" + id);
}
}
static class UserService {
private final UserRepository repository;
UserService(UserRepository repository) {
this.repository = repository;
}
String queryUserName(long id) {
System.out.println("service-queryUserName");
UserEntity entity = repository.findById(id);
return entity.name.toUpperCase();
}
}
static class UserController {
private final UserService service;
UserController(UserService service) {
this.service = service;
}
String handleGetUser(long id) {
System.out.println("controller-handleRequest");
return "{code:0,data:'" + service.queryUserName(id) + "'}";
}
}
public static void main(String[] args) {
UserRepository repository = new MemoryUserRepository();
UserService service = new UserService(repository);
UserController controller = new UserController(service);
String response = controller.handleGetUser(1L);
System.out.println("response=" + response);
}
}7.2 编译并运行
bash
javac LayeredArchitectureDemo.java
java LayeredArchitectureDemo7.3 你应该观察到什么
输出应包含这些关键信息:
text
controller-handleRequest
service-queryUserName
repository-findById
response={code:0,data:'USER-1'}7.4 每一行在验证什么
controller-handleRequest:说明接口层负责接请求和组装响应service-queryUserName:说明业务规则在 Service 层执行repository-findById:说明数据访问细节封装在 Repository 层- 最终响应由 Controller 统一组织:说明各层职责是串联而不是混写
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 拿一个旧项目分析分层是否合理
- 把一个混乱模块按职责拆分
- 画一张“模块依赖方向图”
- 总结模块拆分的判断标准
9. 自测问题
- 为什么分层不是形式问题而是维护性问题?
- 模块拆分最核心的依据是什么?
- 什么样的依赖关系容易导致系统失控?
- 为什么说分层的核心是隔离变化?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 分层是为了职责清晰和变化隔离
- 模块拆分要围绕稳定职责边界
- 依赖方向一旦失控,系统维护成本会迅速上升
- Controller、Service、Repository 各自负责不同层面的工作