示例4 · Erase MVC 版
把单机版 Erase 按 MVC(Model-View-Controller)重构:GameBoard/Shape 只含状态与规则、GameView 只读重绘、GameController 是唯一状态修改入口,组合根 EraseApp 装配三层——可 headless 单测、可替换视图。
示例4 · Erase MVC 版
定位:「MVC 模式」章的配套运行示例。把 示例3 · Erase 网络版 的单体
GameBox(状态+规则+渲染混一类)按 MVC 拆成model / view / controller三个包,演示职责分离如何带来可测试性与可替换性。源码位置:仓库根目录
素材/Erase-MVC/(含src/mvc/...、test/ModelTest.java、docs/1-MVC结构设计方案.md)。
目录
一、运行方式
在 素材/Erase-MVC/ 下(JDK 8+,纯 Swing 无需额外依赖):
cd 素材/Erase-MVC
javac -encoding UTF-8 -d out src/mvc/model/*.java src/mvc/view/*.java src/mvc/controller/*.java src/mvc/EraseApp.java
java -cp out mvc.EraseApp
运行后出现双人对战窗口:P1 用 w s a d,P2 用 ↑ ↓ ← →;菜单可开始/暂停/继续。
二、包结构与职责
| 包 | 类 | 职责 |
|---|---|---|
mvc.model | GameBoard | 游戏状态 + 规则(移动/旋转校验、消行计分、结束判定);状态变更 fireStateChanged() |
Shape | 形状数据 + 移动/旋转/生成(无 Graphics 绘制) | |
GameStateListener | 状态变更通知接口 stateChanged(GameBoard) | |
mvc.view | GameView | 单玩家面板渲染:只读 Model,收到 stateChanged 后 repaint() |
GameFrame | 主窗口:菜单、布局、按键监听挂载(不修改游戏状态) | |
mvc.controller | GameController | 每玩家一个:按键分发 + 200ms Timer 驱动 + 菜单命令,唯一状态修改入口 |
mvc | EraseApp | 组合根:创建并装配 M/V/C(三层唯一交汇点) |
依赖方向(单向、可审计):Controller→Model、View→Model(只读)、Model→View(仅经 GameStateListener 接口回调)。Model 不 import 任何 AWT/Swing 类。
三、对照单机版:改了什么
| 关注点 | 单机版(erase) | MVC 版(mvc) |
|---|---|---|
| 状态 + 规则 | GameBox(混渲染) | model.GameBoard(零 Swing) |
| 渲染 | GameBox.paint* | view.GameView(只读重绘) |
| 计时/按键/菜单 | GameBox.TimerListener + 5 个监听器散落 | controller.GameController(统一入口) |
| 数据类 | Shape(带 Graphics 绘制) | model.Shape(纯数据/规则) |
| 可测试性 | 必须启动窗口人工观察 | test/ModelTest.java headless 自动验证规则 |
四、headless 单元测试
因为 Model 零 Swing 依赖,可以不开窗口直接验证规则:
cd 素材/Erase-MVC
javac -encoding UTF-8 -d out src/mvc/model/*.java test/ModelTest.java
java -cp out ModelTest
ModelTest 构造 GameBoard、调用 handleKey/addscore 等方法并断言状态——这正是 MVC「职责分离」带来的直接收益。
教学衔接:本示例是「MVC 模式」章的运行载体;交互组件
<MVCDataFlow />(见该章)把「一次按键」的跨层数据流动画成时序图。