2019-06-07  Java プログラミング

JavaFXバインディングで宣言的プログラミングを実現する

JavaFX にはバインディングという強力な仕組みがあります。バインディングを導入することで、 従来の命令的だった手続き型プログラミングを宣言的なプログラミングに変えることができます。この記事では、 バインディングとはなにか? 宣言的とはどういうことか? 概念的な説明だけでなく具体的な GUI サンプル ・ アプリケーションを例に解説していきます。

バインディングとは

JavaFX バインディングについて説明する前に以下のコードを見てください。

int a = 3;
int b = 5;
int c = a * b;
System.out.println(c);

このプログラムを実行したときに出力される値が分かりますか?

15?

正解です! プログラミングを齧ったことのある人には簡単な問題でしたね。それでは、 途中にコードを 1 行追加してみます。

int a = 3;
int b = 5;
int c = a * b;

a = 4; //追加しました

System.out.println(c);

c の値を出力する直前に a の値を 4 に変更しています。さて、 画面に出力される値はどうなるでしょうか?

やっぱり、 15?

……正解です。c = a * b は代入文です。c に値を代入した後で a の値を変えても、 c の値は変わりません。プログラマーにとっては当たり前のことですね。

ところが、 この問題をプログラミング経験のない人に出題してみると 「20」 と答える人もいるのです。「c は a と b の積に等しいのだから、 a が 4 になったら、 当然、 c は 20 になる。」 という考えです。c = a * b を代入文ではなく等式として捉えているわけです。プログラミング経験がないからこそ出てくる自然な発想だと思います。

この 「等式として捉える発想」 を、 ただの無知だと斬って捨ててしまったら話はここで終わってしまいます。

「常に c = a * b が成り立つような等式の概念をプログラミングに導入できたらおもしろいかも」 こんなふうに考えられたら素敵です。

とはいえ、 プリミティブな int 型の扱いは Java 言語仕様で決められていますから、 int 型を使っていては、 どうやっても c = a * b の意味を変えることはできません。ひとまず、 型を外して Java ではない架空のコードにしてみましょう。

a = 3
b = 5
c = a * b

a = 4

print c

この架空のコードにおける c = a * b を、 代入文ではなく等式として振る舞わせる方法はあるでしょうか?

int 型では Java の言語仕様に制約されてしまうのでダメでした。それなら、 オブジェクト型を使ってみるのはどうでしょうか? オブジェクト型ならもっと自由に振る舞いを決めることができるはずです。

そんな夢のようなオブジェクト型が…… もうあるんです! それが、 JavaFX バインディングが提供しているクラス群です。

a = 3
b = 5
c = a * b

a = 4

print c

この架空のコードを実行して 「20」 が出力されるようにしたい。JavaFX バインディングを使うとそれが実現できます。JavaFX にはバインディング可能なオブジェクト型がたくさん用意されています。その中の 1 つに IntegerProperty 型があります。これを使ってコードを書き換えてみます。

IntegerProperty a = new SimpleIntegerProperty(3);
IntegerProperty b = new SimpleIntegerProperty(5);

IntegerProperty c = new SimpleIntegerProperty();
c.bind(a.multiply(b));

a.setValue(4);

System.out.println(c.getValue());

コンストラクタやメソッドが出てきたので少し複雑なコードに見えるかもしれませんが、 警戒しないでください。よく見てみると、 前述の架空のコードと概念的にはまったく同じものなんです。

IntegerProperty a = new SimpleIntegerProperty(3);

これは a = 3 に対応するコードです。一言で言えば、 3 という値を持っている数値オブジェクトです。int 型ではなく IntegerProperty というオブジェクト型になったことで、 いくつかの機能を獲得しました。その機能の 1 つが可観測性です。a の値は観測可能になった (値の変化を知ることができる) という意味です。b についても同様です。

IntegerProperty c = new SimpleIntegerProperty();
c.bind(a.multiply(b));

ここが c = a * b を代入文としてではなく等式として機能させる大事なポイントです。

a.multiply(b) は、 a に b を掛けている処理です。Java では演算子をオーバーロードすることができないので、 メソッド呼び出し (multiply) で掛け算を実現しています。JavaFX バインディングに限らず BigDecimal などでもこのような記述が出てくるので、 こういった書き方を見たことがある人もいると思います。

a.multiply(b) はバインディングを返します。バインディングとは 「束縛された値」 のことです。そして、 この 「a と b を掛けるバインディング」 を c にバインドしています。これで、 a または b の値が変化すれば自動的に c の値も変化するようになりました。

c の値が自動的に変わるなんて不思議な魔法のように思えるかもしれません。種明かしをしましょう。バインディングにはリスナーが使われています。Swing を使ったことがある人ならイベントリスナーに馴染みがあると思いますし、 他の Java フレームワークでもリスナーは良く出てきます。それらのリスナーと同じコールバックの仕組みです。

c は a にリスナーを登録して a の値が変わったときに通知を受け取ります。b にもリスナーを登録して b の値が変わったときに通知を受け取ります。c は通知を受け取ると自身の値 (a × b) を再計算します。

a や b にリスナーを登録できるのは IntegerProperty が ObservableValue インターフェースを実装しているからです。ObservableValue インターフェースには addListener(ChangeListener) というメソッドがあり、 変更通知を受け取るリスナーを登録できるようになっています。

ObservableValue を直訳すると 「観測可能な値」 です。a と b が観測可能であるというのは言い換えれば 「ChangeListener が登録できて変更通知ができるオブジェクトである」 それだけのことなんです。

「束縛」 や 「観測可能」 といった聞きなれない言葉が出てきて、 概念的な説明ばかりされると理解が追い付かず投げ出したくなってしまうかもしれません。ですが、 その実装方法に踏み込んで、 従来からあるリスナー技術を使って実現されていることを知れば少し印象が変わってきませんか?

 c = a * b

これを常に成立させるために、 暗黙的にリスナーを登録して裏側で値を再計算してくれる仕組み。それを提供してくれるのが JavaFX バインディングです。

従来のアプリケーション

ここからは JavaFX バインディングを使うとアプリケーションの実装コードがどのように変わるのかを見ていきます。

サンプルとして 「ログイン画面」 を用意しました。

仕様はこうです。

  • ユーザー名とパスワードのテキストボックスがある。
  • ログインボタンは初期状態で無効。押下できない。
  • ユーザー名とパスワードに何か文字を入力するとログインボタンが有効になる。

最初に、 バインディングを使わない従来のアプリケーション実装を提示します。

Main.fxml
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <StackPane xmlns:fx="http://javafx.com/fxml/1" prefWidth="300" prefHeight="200"> <VBox alignment="CENTER" spacing="10"> <GridPane alignment="CENTER" hgap="10" vgap="10"> <Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="ユーザー名" /> <TextField GridPane.rowIndex="0" GridPane.columnIndex="1" fx:id="tfUsername" /> <Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="パスワード" /> <PasswordField GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="pfPassword" /> </GridPane> <Button fx:id="btnLogin" text="ログイン" /> </VBox> </StackPane>
Main.java
package com.example; import java.net.URL; import java.util.ResourceBundle; import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.stage.Stage; public class Main extends Application implements Initializable { public static void main(String[] args) { launch(args); } @FXML TextField tfUsername; @FXML PasswordField pfPassword; @FXML Button btnLogin; @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml")); loader.setController(this); Parent root = (Parent)loader.load(); Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.show(); } @Override public void initialize(URL location, ResourceBundle resources) { btnLogin.setDisable(true); tfUsername.textProperty().addListener(this::tfUsername_onTextChanged); pfPassword.textProperty().addListener(this::pfPassword_onTextChanged); } // このメソッドはユーザー名のテキストが変化したときに呼ばれます protected void tfUsername_onTextChanged( ObservableValue<?> observable, String oldValue, String newValue) { if(newValue.isEmpty() || pfPassword.getText().isEmpty()) { btnLogin.setDisable(true); } else { btnLogin.setDisable(false); } } // このメソッドはパスワードのテキストが変化したときに呼ばれます protected void pfPassword_onTextChanged( ObservableValue<?> observable, String oldValue, String newValue) { if(newValue.isEmpty() || tfUsername.getText().isEmpty()) { btnLogin.setDisable(true); } else { btnLogin.setDisable(false); } } }

よくあるイベントリスナー実装ですね。

  • ユーザー名 (tfUsername) のテキストが変わったときに tfUsername_onTextChanged メソッドが呼ばれます。
  • パスワード (pfPassword) のテキストが変わったときに pfPassword_onTextChanged メソッドが呼ばれます。

tfUsername_onTextChanged メソッド、 pfPassword_onTextChanged メソッドではユーザー名とパスワードのテキストが空かどうかをチェックして、 ログインボタンを有効または無効にしています。

これらのリスナーはテキストが変化したときに呼ばれます。裏を返せば、 テキストが変化しなければリスナーは呼ばれないということです。最初に 「ログイン画面」 を表示したときにはリスナーが呼ばれないので、 リスナーだけではログインボタンの状態を適切に設定できません。

対策として initialize メソッド内で、 明示的に btnLogin.setDisable(true); を呼び出すことでログインボタンを無効状態にしています。

  • ユーザー名のテキストが変わったときにボタンの状態を更新する
  • パスワードのテキストが変わったときにボタンの状態を更新する
  • 初期化処理でボタンを無効状態にする

開発者は、 この 3 つの処理を忘れずに実装しなければなりません。万が一、 初期化処理でボタンを無効状態にするのを忘れてしまったら、 最初だけログインボタンが押せてしまうといったバグが発生してしまいます。

これは、 適切なタイミング (テキスト変化 & 初期表示) で処理を実行するように開発者がコードを記述しなければならないことを意味しています。開発者は、 状態を変えなければいけない状況を常に意識して、 開発者自身が状態を変えるコードを書かなければなりません。

開発者自身が状況に応じた適切なコードを書いていくスタイルを命令的プログラミングと言います。想定しているケースやパターンに不足があって状態不整合が発生する ・ ・ ・ そんなありがちなバグにあなたは悩まされていませんか?

バインディングを導入したアプリケーション

前述のアプリケーションにバインディングを導入していきます。

FXML の内容は変わりません。Main.java は以下のように変わります。

Main.java
package com.example; import java.net.URL; import java.util.ResourceBundle; import javafx.application.Application; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.stage.Stage; public class Main extends Application implements Initializable { public static void main(String[] args) { launch(args); } @FXML TextField tfUsername; @FXML PasswordField pfPassword; @FXML Button btnLogin; @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml")); loader.setController(this); Parent root = (Parent)loader.load(); Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.show(); } @Override public void initialize(URL location, ResourceBundle resources) { btnLogin.disableProperty().bind( tfUsername.textProperty().isEmpty().or(pfPassword.textProperty().isEmpty())); } }

ずいぶんとコードが短くなりました。

イベントリスナーである tfUsername_onTextChanged メソッドと pfPassword_onTextChanged メソッドが無くなっています。それどころか、 処理らしい処理がどこにも書かれていないように見えます。これで本当に同じ動きをするのでしょうか?

プログラムを実行してみると、 確かに動きます。ユーザー名とパスワードを入力するとログインボタンが有効に変わりました。

btnLogin.disableProperty().bind(
	tfUsername.textProperty().isEmpty().or(pfPassword.textProperty().isEmpty()));

bind メソッドが出てきました。ここがポイントです。コードの意味を日本語で書いてみるとこんな感じになります。

ログインボタンが無効 = ユーザー名が空である or パスワードが空である

最初のバインディングの話を思い出してください。ここに出てくる 「=」 は代入ではありません。常に成り立つ等式です。

つまり、 ログインボタンの無効状態 (true/false) は、 「ユーザ名が空である or パスワードが空である」 という論理式 (すなわち真偽値 true/false) と常に同じになります。

ユーザー名とパスワード両方のテキストボックスに文字を入力すればログインボタンが有効になります。ユーザー名のテキストボックスを空にすればログインボタンは再び無効になります。自動的にね。もちろん、 これを実現するために裏方では JavaFX がリスナーを使っていろいろと頑張ってくれているのですが、 そんなことは気にしなくても大丈夫です。

ログインボタンが無効 = ユーザー名が空である or パスワードが空である

このように宣言さえすれば、 あとは 「ユーザー名」 と 「パスワード」 の変化に応じてログインボタンの状態が自動的に変わってくれる、 これがバインディングの威力です。

「ログインボタンの状態を変えるのは、 ユーザー名が変わったとき、 パスワードが変わったとき、 初期化処理の 3 箇所だな」 こんなことを考えなくてよくなります。開発者が状況に応じて状態を変えるコードを書かなくていい。 これはコーディング量を減らせるという単純な話ではありません。ケースやパターンの想定もれに起因するバグが生まれなくなります。

バインディングはバグを減らし品質向上に寄与する宣言的プログラミング技法です。

これはリアクティブプログラミングですか?

はい、 リアクティブプログラミングと呼んで差し支えないと思います。宣言的なバインディングによって、 命令的なコードを書くことなく状態を変化させることができるようになりました。これは十分にリアクティブ (反応的) だと言えます。

ただ、 世間的には、 リアクティブストリームやイベントストリームと呼ばれるなんらかのストリームが登場するものに対してリアクティブプログラミングと呼ぶ風潮もあるようです。

私はリアクティブ宣言自体については好意的に受け止めていますが、 RxJava に代表されるストリームありきのリアクティブプログラミングに対してはいまだに懐疑的です。

RxJava 等は、 ストリームを主体としたプログラミングの複雑さ ・ 難解さが他のメリットを上回っているように思うのです。ストリームを主体としたリアクティブプログラミングはパラダイムシフトをもたらすとも言われていますので、 その新しいパラダイムを私が十分に理解できていないだけなのかもしれません。なので、 あまり否定的なことは言いません。

バインディングは、 ストリームとは違った面から、 宣言的でリアクティブ (反応的) なプログラミングを実現しています。リアクティブには関心があるけどストリームがややこしくて投げ出してしまった (私のような) 人にも、 バインディングはオススメしたいプログラミング技法です。ストリームを使わなくたってリアクティブプログラミングはできます!

サンプル・プログラム

サンプル ・ プログラムのソースコードは下記のリンクからダウンロードできます。

最終更新日 2024-12-13
この記事を共有しませんか?
ブックマーク ポスト