元数据卡
- 前置知识:第3章(需求分析与架构设计)
- 预计时间:45 分钟
- 核心难度:进阶
- 完成标志:能设计三层/四层架构,理解 MVC 三种变体的适用场景
你的进度
你的第一台机器:1500 个零件塞在一个炉子里。所有齿轮、弹簧、杠杆直接焊接在一起。后来你分了区:动力区、传动区、控制区。
但时间长了你发现:控制区里混了传动逻辑,传动区里又有控制线路。功能都能跑,但维修工不知道“该拆哪一块”。工匠之都的规范手册里写:分层。 你的任务
大到企业应用、小到个人项目,你都需要一种"层的划分"——把代码按职责分成若干个横向的层次,每层只和相邻层通信。这叫作层架构,最常见的例子是三位一体的 MVC(Model-View-Controller)。这章从三层架构出发,讲到 MVC、MVP、MVVM——每一种变体解决的是同一个问题:当 UI 和逻辑越来越复杂时,你怎么把屏幕上的视觉和背后的数据/行为拆开。
本章分层
- 必读:三层架构、MVC
- 选读:MVP、MVVM
- 进阶:四层架构与领域层
本章不会要求你掌握
- 前端 MVVM 框架的完整实现(Vue Reactivity 原理)
- CQRS(第15章讲)
破局 · 溯源
你的 PlayerController 里有一段代码:
// 坏:controller 里包含了业务逻辑和 SQL
public void registerPlayer(HttpRequest req, HttpResponse res) {
String name = req.getParam("name");
if (name.length() < 2) {
res.send("名字太短");
return;
}
// 直接写 SQL
String sql = "INSERT INTO players (name, level) VALUES (?, 1)";
jdbcTemplate.update(sql, name);
res.send("注册成功");
}这段代码在 JSP/Servlet 时代很常见——但是把展示(HTTP 响应)、业务逻辑(名字长度校验)、数据访问(SQL)揉在一个方法里。好处是快——15 行写完。坏处是:改界面颜色要改这个文件、改注册规则要改这个文件、换数据库也要改这个文件。
第一层:三层架构——展示、业务、数据各自安好
+---------------------------+
| 展示层 (Presentation) |
| - Controller / View |
+---------------------------+
|| 调用
+---------------------------+
| 业务层 (Business Logic) |
| - Service / Domain |
+---------------------------+
|| 调用
+---------------------------+
| 数据访问层 (Data Access) |
| - Repository / DAO |
+---------------------------+每层只依赖它正下方的层。展示层不能直接访问数据访问层——必须经过业务层。这个约束防止了"查询数据库的逻辑四处开花"。
用三层架构重构你的注册:
// 展示层
@RestController
@RequestMapping("/players")
public class PlayerController {
private final PlayerService playerService;
public PlayerController(PlayerService playerService) {
this.playerService = playerService;
}
@PostMapping
public ResponseEntity<String> register(@RequestBody RegisterRequest request) {
playerService.register(request.getName(), request.getLevel());
return ResponseEntity.ok("注册成功");
}
}
// 业务层
@Service
public class PlayerService {
private final PlayerRepository playerRepository;
public PlayerService(PlayerRepository playerRepository) {
this.playerRepository = playerRepository;
}
public void register(String name, int level) {
if (name == null || name.length() < 2) {
throw new IllegalArgumentException("名字至少 2 个字符");
}
if (playerRepository.findByName(name).isPresent()) {
throw new IllegalStateException("重名");
}
Player player = new Player(name, level);
playerRepository.save(player);
}
}
// 数据访问层
@Repository
public class PlayerRepository {
private final JdbcTemplate jdbc;
public PlayerRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public Optional<Player> findByName(String name) {
return jdbc.query("SELECT * FROM players WHERE name = ?",
new BeanPropertyRowMapper<>(Player.class), name)
.stream().findFirst();
}
public void save(Player player) {
jdbc.update("INSERT INTO players (name, level) VALUES (?, ?)",
player.getName(), player.getLevel());
}
}你注意到变化:PlayerController 只有 10 行,只做一件事——接收 HTTP 请求、转发、返回响应。校验逻辑在 PlayerService。数据库操作在 PlayerRepository。
三层的核心约束:层与层之间的依赖是单向的。Controller 依赖 Service,Service 依赖 Repository。反过来不行——Repository 不应该导入 Controller 的类。
第二层:四层架构——引入领域层
三层架构有一个隐含问题:PlayerService 里的一部分逻辑属于业务规则("名字至少 2 个字符、不能重名"),另一部分只是编排("先找、再存、再返回")。
如果项目继续变大,PlayerService 也会变成上千行的"上帝服务"。四层架构把"核心业务规则"单独抽成一个领域层:
+---------------------------+
| 接口层 (Interface) |
| - Controller / API |
+---------------------------+
| 应用层 (Application) |
| - Service / Use Case |
+---------------------------+
| 领域层 (Domain) |
| - Entity / Value Object |
+---------------------------+
| 基础设施层 (Infrastructure)|
| - Repository / DB / MQ |
+---------------------------+领域层是无框架依赖的纯 Java/Python 类。它不知道 HTTP、不知道 Spring、不知道数据库。
// 领域层:纯业务逻辑,没有任何框架注解
public class Player {
private PlayerId id;
private PlayerName name; // Value Object
private Level level;
public Player(PlayerName name, Level level) {
this.id = PlayerId.generate();
this.name = name;
this.level = level;
}
public void levelUp() {
this.level = this.level.increment();
}
public boolean canEnterTournament(Tournament tournament) {
return this.level.value() >= tournament.getMinLevel();
}
}
// PlayerName 作为值对象,封装了"名字"的规则
public record PlayerName(String value) {
public PlayerName {
if (value == null || value.length() < 2) {
throw new IllegalArgumentException("名字至少 2 个字符");
}
}
}注意 PlayerName 是一个 value object——它自身封装了"名字"的校验规则。PlayerService 里不再需要 if (name.length() < 2),因为创建 PlayerName 时就检查了。
第三层:MVC——模型-视图-控制器
你写了一个比赛控制面板。用户点击"开始比赛",UI 调用后端 API。你的做法是把 HTML、CSS、JS 放在一起——这是最朴素的 Web 应用。真正需要解耦的是三个概念:
- Model(模型):数据和业务逻辑。在 Spring MVC 中对应
Player、Tournament等 domain 对象。 - View(视图):视觉展示。HTML 模板、JSON 序列化。
- Controller(控制器):接收用户输入,协调 Model 和 View。
经典 MVC 的工作流:
Controller ← 用户输入
↓
Controller 修改 Model
↓
Model 通知 View 更新
↓
View 读取 Model 并渲染// Spring MVC 例子
@Controller
@RequestMapping("/matches")
public class MatchController {
private final MatchService matchService;
@GetMapping("/{id}")
public String showMatch(@PathVariable String id, Model model) {
Match match = matchService.findById(id);
model.addAttribute("match", match);
return "match-detail"; // 视图名
}
}在传统的 Spring MVC 里,Controller 返回视图名,View(JSP/Thymeleaf)渲染 HTML。今天的前后端分离场景里,Controller 通常返回 JSON,View 是前端的职责。
第四层:MVP——模型-视图-展示器
MVC 在实践中有一个常见问题:Controller 和 View 的耦合。测试 View(需要浏览器/DOM)很难。MVP 把 View 的角色降到最低——View 只负责渲染,展示器(Presenter)处理所有逻辑。
View ←→ Presenter → Model// MVP 模式
// View 接口(抽象化,方便测试)
public interface MatchListView {
void showMatches(List<MatchSummary> matches);
void showError(String message);
}
// Presenter
public class MatchListPresenter {
private final MatchListView view;
private final MatchService service;
public MatchListPresenter(MatchListView view, MatchService service) {
this.view = view;
this.service = service;
}
public void onLoad() {
try {
List<Match> matches = service.getAll();
List<MatchSummary> summaries = matches.stream()
.map(m -> new MatchSummary(m.getId(), m.getStatusName()))
.toList();
view.showMatches(summaries);
} catch (Exception e) {
view.showError("加载失败");
}
}
}
// Android/JavaFX 开发中常用——测试 Presenter 不需要 UI第五层:MVVM——模型-视图-视图模型
MVVM 进一步优化——ViewModel 是 View 的"状态投影",View 自动绑定 ViewModel 的状态变化。Web 框架中的 React(通过 state 和 props)、Vue(响应式 data)、Angular(RxJS)本质上是 MVVM 的变体。
View ←--- 数据绑定 ---→ ViewModel → Model视图模型(ViewModel)暴露可观察的状态:
// 抽象概念(以 Vue 的响应式为例)
public class MatchViewModel {
private final ObservableProperty<String> status = new ObservableProperty<>("等待中");
private final ObservableProperty<String> playerA = new ObservableProperty<>("");
private final ObservableProperty<String> playerB = new ObservableProperty<>("");
public void load(String matchId) {
Match match = matchService.findById(matchId);
status.set(match.getStatusName());
playerA.set(match.getPlayerA());
playerB.set(match.getPlayerB());
}
}
// View 自动绑定 ViewModel 的变化
// <div>{{ viewModel.status }}</div> ← 自动更新MVVM 的核心优势:ViewModel 不依赖 View 的具体实现(Web、桌面、移动都可以用同一个 ViewModel)。端到端测试的范围缩小到 ViewModel 层,View 的测试轻量得多。
常见陷阱
陷阱一:分层变成了"包层级"而不是"依赖层级"。 你建了 controller、service、repository 包,但 Controller 直接调用了 Repository——"只因为我想省事,就绕过 Service 层。" 每次绕过都是对架构的一次破坏,累积多了层就失去了意义。团队规范:cross-layer 调用必须经过 Service 审批。
陷阱二:Service 层成为 God Class。 Controller 太薄了,于是所有逻辑都塞在 Service 里——一个 Service 500 行。重构:提取领域层的业务规则,Service 只做编排(协调多个领域对象)。
陷阱三:MVP 使得 Presenter 太胖。 所有逻辑都塞在 Presenter 里,View 变成纯展示壳。当 Presenter 超过 300 行时,考虑把业务逻辑下移到 Model/Service 层。
陷阱四:层与层之间用 JSON/Map 传递数据。 Controller 把 HttpServletRequest 传到 Service,Service 再把 Request 传给 Repository——层与层的边界被模糊了。每层之间应该用定义好的 DTO(数据传输对象)。
// Controller 用 RegisterRequest(DTO)
// Service 用 Player(Domain)
// Repository 用 SQL Row 映射
// 每层的"语言"不同——不能混用通关挑战
- 热身:从你的项目里找一个 Controller,检查它有没有跳过 Service 直接调用 Repository。如果有,加上 Service 层。
- 挑战:把你的项目重构为四层架构(接口层/应用层/领域层/基础设施层)。提取领域层到独立 package/module,确保领域层没有框架依赖。
- 观察:看看你的前端框架(React/Vue)是 MVVM 的哪种变体。找到 ViewModel 对应哪个概念(Vue 的 data/composable?React 的 hooks/state?)。
旅人笔记
分层架构是最朴素也最有效的组织方式——展示层处理输入输出,业务层处理规则,数据层处理持久化。MVC 及其变体(MVP、MVVM)在分层的基础上进一步把 UI 和逻辑分开。不管项目大小,先画出层次图再写代码,结构自然清晰。
下一站预告
分层的方向是垂直切分。但如果"外部"的细节(数据库、HTTP、UI)不断渗透进你的核心业务逻辑怎么办?下一章——六边形与整洁架构。