導入
JavaFXアプリを素朴に書くと、ボタンのクリックハンドラの中に業務ロジックやデータ操作を直接書いてしまいがちです。これでは画面を起動しないとロジックを1行もテストできません。MVVM(Model-View-ViewModel)は、この問題を「役割を3つに分ける」ことで解決する設計です。
説明
flowchart LR
U["ユーザー操作"] --> V["View<br/>(FXML・見た目)"]
V <-->|バインディング| VM["ViewModel<br/>(画面の状態と操作)"]
VM -->|呼び出す| M["Model<br/>(Entity・Service など業務ロジック)"]
M -->|結果を返す| VM
3つの層の責務は、次のように分かれます。
| 層 | 持つもの | 持たないもの |
|---|---|---|
| View | 見た目(FXML)、レイアウト、色 | 業務ロジック、データアクセス |
| ViewModel | 画面の状態(入力中の文字・一覧・ボタンの活性)、画面の操作 | JavaFXの画面部品そのもの |
| Model | Entity・値オブジェクト・Service・Repository | 画面の都合 |
一番大事なのは ViewModelがJavaFXの画面部品(Button や TextField)を一切知らない ことです。ViewModelが知っているのは「タイトルという文字列」「タスクの一覧」「追加できるかどうか」といった画面の状態だけ。だからViewModelは、画面を起動しなくても普通のJavaのクラスとしてテストできます。このコースで書くテストのほとんどが、まさにこの性質のおかげで成り立っています。
素朴な書き方との比較
コントローラに直接書くと、こうなります。
// アンチパターン: 画面のイベントハンドラに業務ロジックが直書きされている
@FXML
private void onAddClicked() {
String title = titleField.getText();
if (title == null || title.isBlank()) return;
if (title.length() > 50) return;
tasks.add(new TaskItem(UUID.randomUUID().toString(), title, false));
titleField.clear();
countLabel.setText("残り " + tasks.size() + " 件");
}
「50文字以内」というルールをテストしたいだけなのに、titleField と countLabel、つまり画面がないと1行も動かせません。MVVMではこれを分けます。
// ViewModel: 画面部品を知らない。ただのJavaのクラスなのでテストできる
public void add() {
if (!canAdd()) return;
taskService.add(newTitle);
setNewTitle("");
}
public boolean canAdd() {
return !newTitle.isBlank();
}
MVVMとMVCの違い
Model-View-Controller(MVC)との一番の違いは、ViewとViewModelをつなぐのが「バインディング」であることです。MVCのControllerは「Viewを取ってきて値を入れる」という書き方をしますが、MVVMのViewModelはViewを知りません。ViewModelが自分の状態を変えて「変わったよ」と知らせると、その通知を購読しているViewが勝手に描き直します。この向きの違いが、テストのしやすさを生みます。
まとめ
- MVVMは View(見た目)/ViewModel(画面の状態と操作)/Model(業務ロジック)の3層
- ViewModelはJavaFXの画面部品を知らない。だから画面なしでテストできる
- ViewとViewModelはバインディングでつながる。ViewModelからViewを直接触らない
次回: この3層を実際のパッケージ構成に落とし込みます。