概要
GoogleのJava用DIに「Google Guice」がある。
そちらを使用して、JavaFX8でのテスト時のロジックの切り替えができないかと検証してます。
とりあえず今回は、Guiceの基本的な使い方です。
詳しい解説は「Google Guice 使い方メモ」が良いです。
全ソースはこちら。
必要なjar
下記のjarをダウンロードし、クラスパスに加えました。
・guice-4.0-beta5.jar
https://github.com/google/guice/wiki/Guice40
・guava-18.0.jar
https://code.google.com/p/guava-libraries/
・javax.inject.jar
https://code.google.com/p/dependency-shot/downloads/detail?name=javax.inject.jar&can=2&q=
・com.springsource.org.aopalliance-1.0.0.jar
http://www.java2s.com/Code/Jar/c/Downloadcomspringsourceorgaopalliance100jar.htm
Mavenでは、下記を加えれば良いそうです(未実施)
(http://mvnrepository.com/artifact/com.google.inject/guice/4.0-beta より)
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.0-beta</version>
</dependency>
作成するクラス
- ロジック→画面の処理を受けて、実処理をするクラス
- コントローラ→画面のクリックなどのイベントに対応するクラス
- メイン→画面のクラス
コントロール内のロジックのインスタンスを、メインから「依存性の注入」をするということです。
ロジッククラス
ロジッククラスとなるのは、3種類。
- LogicDummy:インターフェース
- LogicImplement:実際に使用されるロジックの実装
- LogicDummy: テストで使用されるロジックの実装
内容は、
add: 一つ目の引数と二つ目の引数を足した数を返す
substract: 一つ目の引数から二つ目の引数を引いた数を返す
です。
インターフェースはこちら。
package guice;
import com.google.inject.ImplementedBy;
@ImplementedBy(LogicImplement.class)
public interface LogicInterface {
String add(int a, int b);
String substract(int a, int b);
}
@ImplementedBy(LogicImplement.class)
でLogicInterfaceが注入されるときに特に設定がされていないようであれば、LogicImplementを使用してね、とデフォルトを設定してます。
実挙動としては下記のようになります。
package guice;
public class LogicImplement implements LogicInterface {
@Override
public String add(int a, int b) {
return String.valueOf(a + b);
}
@Override
public String substract(int a, int b) {
return String.valueOf(a - b);
}
}
単純です。
テスト用のロジックとしては、下記のようです。
package guice;
public class LogicDummy implements LogicInterface {
@Override
public String add(int a, int b) {
return "(" + a + "足す" + b + ")";
}
@Override
public String substract(int a, int b) {
return "(" + a + "引く" + b + ")";
}
}
画面のクリック時には、このテスト用のロジックからのダミーメッセージを画面に表示させるなどして、画面のイベントが各ロジックに伝わり、その結果が画面にちゃんと表示されることを、画面に実際入力してみて、確認します。
(画面からの入力値がちゃんとロジックにわたり、その結果が正しく画面に反映させることを、自動テストでできるのでしょうか?ご存知の方、お教えください)
コントロール
画面のイベント処理(ボタンのクリック)や描画処理(結果の表示)などを担当する。
package guice;
import com.google.inject.Inject;
public class Controller {
@Inject
private LogicInterface logic;
public String add(int a, int b){
return logic.add(a, b);
}
public String substract(int a, int b){
return logic.substract(a, b);
}
}
DI抽出対象のLogicIntefaceを定義してます。抽出対象は@Injectアノテーションで表します。
コントロールのクラスは、ロジッククラスの足し算/引き算の結果を返す(だけ)。
メイン文
コントロールを呼び出すmain文です。
package guice;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class TestMain {
public static void main(final String[] args) {
// DIの注入を行うインスタンス
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
if (0 < args.length && args[0].equals("TEST")){
// LogicInterfaceには、LogicDummyを注入することを定義する
bind(LogicInterface.class).to(LogicDummy.class);
}
}
});
Controller controller = new Controller();
// コントロールの@Injectアノテーションのある変数に注入を行う
injector.injectMembers(controller);
System.out.println("3 + 5 = " + controller.add(3, 5));
System.out.println("5 - 3 = " + controller.substract(5, 3));
}
}
Guiceの「オンデマンドでインジェクションする」を利用しています。
引数の一つ目が"TEST"であれば、LogicInterfaceには、LogicDummyを注入されます。そうでなければ、@ImplementedByで定義したデフォルトの方が注入されます。こちらの定義もなければ、nullが注入されます。
>java guice.TestMain
3 + 5 = 8
5 - 3 = 2
>java guice.TestMain TEST
3 + 5 = (3足す5)
5 - 3 = (5引く3)
JavaFX8に適用する
コマンドラインを参考に、JavaFX8のメインウィンドウのクラスに適応したバージョンです。
まず、コントローラクラス。fxmlで定義した外観に対して、処理を追加してます。
package guice;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import com.google.inject.Inject;
public class MainController implements Initializable{
@FXML private TextField input1;
@FXML private TextField input2;
@FXML private Label lblAdd;
@FXML private Label lblSubstract;
private StringProperty propertyAdd = new SimpleStringProperty();
private StringProperty propertySubstract = new SimpleStringProperty();
@FXML private Button btn;
@Inject
private LogicInterface logic;
@FXML
public void onBtnClicked(ActionEvent event){
int intInput1 = Integer.parseInt(input1.getText());
int intInput2 = Integer.parseInt(input2.getText());
propertyAdd.set(logic.add(intInput1, intInput2));
propertySubstract.set(logic.substract(intInput1, intInput2));
}
@Override
public void initialize(URL paramURL, ResourceBundle paramResourceBundle) {
lblAdd.textProperty().bind(propertyAdd);
lblSubstract.textProperty().bind(propertySubstract);
}
}
public void onBtnClicked(ActionEvent event){
で、ボタンをクリックされたときのイベントを定義してます。
二つのテキストフィールドから入力値を取得し、足した結果と引いた結果を二つのラベルに出力します。
public void initialize(URL paramURL, ResourceBundle paramResourceBundle) {
でコントロールとFXMLファイルの初期化が終わったときの処理です。ラベルの値をプロパティにしてバインドしてみました。
メインクラスとしては、以下のようです。
package guice;
import java.util.List;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
FXMLLoader loader = new FXMLLoader();
loader.load(getClass().getResource("MainApplication.fxml").openStream());
BorderPane root = loader.getRoot();
Scene scene = new Scene(root);
// メイン文の引数を取得する
final List<String> args = getParameters().getRaw();
// DIの注入の定義
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
if (0 < args.size() && args.get(0).equals("TEST")){
bind(LogicInterface.class).to(LogicDummy.class);
}
}
});
// コントロールを取得し、DIの注入を実施する
injector.injectMembers(loader.getController());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
コントローラを取得するのにFMLLoaderがいるので、
FXMLLoader loader = new FXMLLoader();
でloaderを取得し、DI抽出対象のコントローラを
loader.getController()
で取得してます。
メイン文の引数は、下記で取得できる。
final List<String> args = getParameters().getRaw();
実際に実行してみるとこんな感じです。
考え事
実際にどのように実運用とテスト用のDIの注入のクラスを定義するか、が疑問点です。おそらく今回の例だけでなくDI全体的なことで(Guice以外のDIは固定したxmlに定義するから、どこに定義するか、では悩まなくて良いのでしょうか)。
今回は実装のみの実験なので、起動時の引数で切り替わるようにしました。実際の運用ではどのようにしているのでしょうか。
- main文からの起動は常にテスト用と見なし、main文内にテスト用のクラスを書く。実際は他の画面から起動されたり、画面が遷移されてくる。(画面が一つでないアプリケーションでは、できそうです)
- DI注入用のクラスを一つ作成し、そこに実稼働用とテスト用のクラスを定義する
業務でDIを使用しているプロジェクトに参画しましたが、それでは、業務名「Sample」を実施するプログラムとして、ISample.javaというインターフェースを作る。インターフェース名から最初のIをとって、それにImpleをつけたSampleImpl.javaというクラスを作成する、という実装でした。
結論
Guiceを使用して、GUIがちゃんとしたロジッククラスに結びついているかをテストする方法を検討してみました。ロジック自体は、JUnit等で十分テストするので完璧、なはず。
突っ込みや意見は歓迎していますので、ご記入頂ければ、検討したいと思います。