本文へスキップ
BecomeCoder

Java MVVMコース · 第6章 ViewModel ― 画面の状態と操作 · レッスン23

Command ― ボタンの操作をオブジェクトにする

ブラウザで完結

導入

「追加」ボタンを押したら何が起きるべきか――それをViewModelに伝える仕組みがCommandです。Viewのボタンにイベントハンドラを直接書くのではなく、ViewModelが持つCommandにボタンをつなぎます。こうすればViewModelはJavaFXを知らずに済み、テストも書けます。ただしCommandを毎回1から実装するのは大変なので、汎用のヘルパーRelayCommandを作ります。

図解

flowchart LR
    B["Button(View)"] -->|クリック| C["Command<br/>(ViewModelが持つ)"]
    C -->|execute| L["ViewModelの処理本体"]
    style C fill:#e8f5e9

説明

Command.java

public interface Command {
    void execute();
    default boolean canExecute() { return true; }
}

RelayCommand.java

import java.util.function.BooleanSupplier;

public class RelayCommand implements Command {
    private final Runnable action;
    private final BooleanSupplier canExecuteCheck;

    public RelayCommand(Runnable action) {
        this.action = action;
        this.canExecuteCheck = null;
    }

    public RelayCommand(Runnable action, BooleanSupplier canExecute) {
        this.action = action;
        this.canExecuteCheck = canExecute;
    }

    public void execute() {
        action.run();
    }

    public boolean canExecute() {
        return canExecuteCheck == null || canExecuteCheck.getAsBoolean();
    }
}
Command greetCommand = new RelayCommand(() -> System.out.println("こんにちは"));
System.out.println(greetCommand.canExecute());   // true(canExecute未指定なら常に実行可)
greetCommand.execute();                          // こんにちは
  • Commandexecute(実行)・canExecute(実行可能か、既定はtrue)の2つだけを持つ、とても小さいインターフェース
  • RelayCommandはラムダ式を受け取るだけでCommandを満たせる汎用実装。ViewModelごとに専用クラスを書かずに済む
  • canExecuteを渡さないコンストラクタでは常に実行可能(canExecuteCheck == null

やってみよう

canExecute() -> falseを渡すコマンドを作り、execute()を呼んでも動作すること自体は止まらない(canExecuteはあくまで「実行してよいか」を返すだけで、executeを自動でブロックはしない)ことを確認してみましょう。

演習

エディタはCommand.java(完成済み)・RelayCommand.java(今日のTODO)・RelayCommandTest.java(テスト)の3つのタブに分かれています。

public interface Command {
    void execute();
    default boolean canExecute() { return true; }
}
import java.util.function.BooleanSupplier;

// TODO: Command を実装する RelayCommand を完成させてください
//       ・コンストラクタで実行本体(Runnable)を受け取る
//       ・もう1つのコンストラクタで、実行本体に加えて実行可否(BooleanSupplier)を受け取る
//       ・canExecute() は、実行可否を渡していなければ常に true、渡していればその結果を返す
//       ・execute() は実行本体を呼ぶ
public class RelayCommand implements Command {
}
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;

public class RelayCommandTest {
    @Test
    void canExecuteMishiteiNaraTsuneniJikkoKanoDeExecuteGaYobareru() {
        List<String> executed = new ArrayList<>();
        Command command = new RelayCommand(() -> executed.add("run"));

        assertTrue(command.canExecute(), "canExecuteが未指定ならtrue");

        command.execute();
        command.execute();

        assertEquals(2, executed.size(), "executeのたびに実行本体が呼ばれる");
    }

    @Test
    void canExecuteNoKekkaGaJikkoKahiniHanjiSareru() {
        boolean[] allowed = {false};
        Command command = new RelayCommand(() -> {}, () -> allowed[0]);

        assertFalse(command.canExecute(), "canExecuteがfalseを返す間は不可");

        allowed[0] = true;

        assertTrue(command.canExecute(), "trueになれば実行可能になる");
    }
}
  • 期待される結果: 2件のテストがすべて成功
ヒント1を見る

コンストラクタでactioncanExecuteCheckをフィールドに保存します。canExecuteを受け取らないコンストラクタではcanExecuteChecknullにします

ヒント2を見る

public boolean canExecute() { return canExecuteCheck == null || canExecuteCheck.getAsBoolean(); } public void execute() { action.run(); }

まとめ

  • CommandはViewからの「実行」をViewModelへ橋渡しする、executecanExecuteだけの小さいインターフェース
  • RelayCommandはラムダを渡すだけで使える汎用実装
  • ViewModelはJavaFXに依存せず、Commandを介した操作を単体テストできる

次回: 「今は追加できない」をcanExecuteに反映させます。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。