導入
「追加」ボタンを押したら何が起きるべきか――それを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(); // こんにちは
Commandはexecute(実行)・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を見る
コンストラクタでactionとcanExecuteCheckをフィールドに保存します。canExecuteを受け取らないコンストラクタではcanExecuteCheckをnullにします
ヒント2を見る
public boolean canExecute() { return canExecuteCheck == null || canExecuteCheck.getAsBoolean(); } public void execute() { action.run(); }
まとめ
CommandはViewからの「実行」をViewModelへ橋渡しする、executeとcanExecuteだけの小さいインターフェースRelayCommandはラムダを渡すだけで使える汎用実装- ViewModelはJavaFXに依存せず、
Commandを介した操作を単体テストできる
次回: 「今は追加できない」をcanExecuteに反映させます。