Skip to content

元数据卡

  • 前置知识:第3章(需求分析与架构设计)
  • 预计时间:45 分钟
  • 核心难度:进阶
  • 完成标志:能设计三层/四层架构,理解 MVC 三种变体的适用场景

你的进度

你的第一台机器:1500 个零件塞在一个炉子里。所有齿轮、弹簧、杠杆直接焊接在一起。后来你分了区:动力区、传动区、控制区。

但时间长了你发现:控制区里混了传动逻辑,传动区里又有控制线路。功能都能跑,但维修工不知道“该拆哪一块”。工匠之都的规范手册里写:分层。 你的任务

大到企业应用、小到个人项目,你都需要一种"层的划分"——把代码按职责分成若干个横向的层次,每层只和相邻层通信。这叫作层架构,最常见的例子是三位一体的 MVC(Model-View-Controller)。这章从三层架构出发,讲到 MVC、MVP、MVVM——每一种变体解决的是同一个问题:当 UI 和逻辑越来越复杂时,你怎么把屏幕上的视觉和背后的数据/行为拆开。

本章分层

  • 必读:三层架构、MVC
  • 选读:MVP、MVVM
  • 进阶:四层架构与领域层

本章不会要求你掌握

  • 前端 MVVM 框架的完整实现(Vue Reactivity 原理)
  • CQRS(第15章讲)

破局 · 溯源

你的 PlayerController 里有一段代码:

java
// 坏: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        |
+---------------------------+

每层只依赖它正下方的层。展示层不能直接访问数据访问层——必须经过业务层。这个约束防止了"查询数据库的逻辑四处开花"。

用三层架构重构你的注册:

java
// 展示层
@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、不知道数据库。

java
// 领域层:纯业务逻辑,没有任何框架注解
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 中对应 PlayerTournament 等 domain 对象。
  • View(视图):视觉展示。HTML 模板、JSON 序列化。
  • Controller(控制器):接收用户输入,协调 Model 和 View。

经典 MVC 的工作流:

Controller ← 用户输入

Controller 修改 Model

Model 通知 View 更新

View 读取 Model 并渲染
java
// 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
java
// 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)暴露可观察的状态:

java
// 抽象概念(以 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 的测试轻量得多。


常见陷阱

陷阱一:分层变成了"包层级"而不是"依赖层级"。 你建了 controllerservicerepository 包,但 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(数据传输对象)。

java
// 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)不断渗透进你的核心业务逻辑怎么办?下一章——六边形与整洁架构。

Built with VitePress | Software Systems Atlas