起因

项目最早的写法是典型的”胖 Controller”:View(FXML 对应的 Controller)里既管 UI 交互,又直接调 Service 加载数据、刷表格、维护选中状态。功能一多,单个 Controller 几千行,加载数据的逻辑和 UI 操作揉一块儿,几乎没法单测,改一处怕动全身。

拆分

按 MVVM 把职责切开:

  • View 只管 UI 绑定和用户交互,从 UI 组件里抠纯数据(比如表格当前选中的行),不含业务。
  • ViewModel 持有状态(Property / ObservableList),负责数据加载的调度和参数构建,对 View 暴露可绑定的属性。
  • Service 是纯逻辑,不碰 JavaFX,能在任意线程跑、能单测。

核心动作就一个:把数据加载从 View 剥到 ViewModel。原来 Controller 里 service.load() 之后跟一大串 UI 刷新,现在改成 ViewModel 加载完更新自己的 ObservableList,View 靠绑定自动刷新:

class RadarViewModel {
val files: ObservableList<RadarFileVo> = observableListOf()

fun load(projectId: String) = viewModelScope.launch {
files.setAll(service.loadFiles(projectId)) // 加载在 ViewModel
}
}

View 只要绑上 files,在合适的时候调一下 vm.load(id) 就行,完全不关心数据怎么来的。

收益

改完之后最直接的感受:View 薄了一大截,Controller 回归到”只做 UI”该干的事;ViewModel 和 Service 都不依赖 JavaFX 的 UI 组件,能脱离界面写单测,尤其渲染参数构建、数据转换这种纯逻辑,测起来很舒服。后来做”截图改 Java2D 纯数据渲染”的时候也沾了光——正因为渲染参数在 ViewModel 层就组装好了,Service 层才能完全不碰 UI 直接画。

我的体会是,MVVM 这东西关键不在套个名字,而是真把”状态 + 数据调度”从 View 里抽出来。一旦抽出来,可测性和复用性立马就不一样了,后面别的模块照着改也都顺。