概要
少し前に、「JavaFXで十分Data Bindingできるじゃん!」といった記事を書きました。
Java+JavaFX+MVVMでストップウォッチアプリを作成してみた所感
ただ、これじゃ足りない!足りないんだよ! 具体的にはジェネリクスで書きたいんだよ!
StringProperty
だのBooleanProperty
だの、お前それ実装してて一ミリもクソだと思わなかったのかとJava開発者(SunかOracle)を小一時間問い詰めたい命名がされているクラスを眺めていて思いました。
「じゃあジェネリクスの練習がてらReactiveProperty作ろう」と。
※最終的なコードは文末に貼ってあります
ベースを何にするか
とりあえず、Property<T>
型のインスタンスを内に持ちながら、インターフェースProperty<T>
を継承することにしました。実体は……Property<T>
を継承しているSimpleObjectProperty<T>
でいいでしょう、多分。
public class ReactiveProperty<T> implements Property<T> {
private Property<T> value;
// コンストラクタ
public ReactiveProperty(){
value = new SimpleObjectProperty<>();
}
public ReactiveProperty(T t){
value = new SimpleObjectProperty<>(t);
}
}
Property<T>
を継承したので当然各種メソッドを実装する必要がありますが、全部内部のインスタンスに丸投げしました(冗長なのでコードは略)。ただ、public void setValue(Object value){this.value.setValue((T)value);}
のように、Object
型が引数なメソッドの場合はキャストを書かなければならないのが気持ち悪いですね……。
ReactivePropertyっぽいメソッドを実装しよう
一応代替物を目指しているので、Select
やSubscribe
などの主要なメソッドは実装しておきたいものです。通知方法ですが、内部のインスタンスがaddListener
メソッドを持っているので、そちらに追記することにしました。これだけで、ちゃんと通知機能が働いてくれるのは驚きですね。
ただ、Javaには「拡張メソッド」の概念が存在しませんので、Select
した結果がいきなりReactiveProperty<T>
になりますし、ToReactiveProperty
はStringPropertyなどをReactiveProperty<T>
に置き換えるためだけのメソッドになりました。
public class ReactiveProperty<T> implements Property<T> {
// ReactivePropertyからReactivePropertyを作成
public <R> ReactiveProperty<R> select(Function<T, R> func){
ReactiveProperty<R> retVal = new ReactiveProperty<>(func.apply(this.getValue()));
this.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply((T)newValue));});
return retVal;
}
// 2つのReactivePropertyからReactivePropertyを作成
public <U, R> ReactiveProperty<R> combineLatest(ReactiveProperty<U> u, BiFunction<T, U, R> func){
ReactiveProperty<R> retVal = new ReactiveProperty<>(func.apply(this.getValue(), u.getValue()));
this.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply((T)newValue, u.getValue()));});
u.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply(this.getValue(), (U)newValue));});
return retVal;
}
// ◯◯PropertyからReactivePropertyを作成
public static <R> ReactiveProperty<R> ToReactiveProperty(Property<R> r){
ReactiveProperty<R> retVal = new ReactiveProperty<>(r.getValue());
r.addListener((sb, oldValue, newValue)->{retVal.setValue(r.getValue());});
return retVal;
}
// 変更通知機能
public void subscribe(Consumer<T> cons){
this.addListener((sb, oldValue, newValue) -> {cons.accept(this.getValue());});
}
}
まとめ
上記のように実装することで、ジェネリクスによる通知付きプロパティを作成することができました。JavaFXプロジェクトに持ち込んで使用してみたところ、Property<T>
を継承しているので、コントロールのプロパティとちゃんとData Bindingできますので、十分役に立つものと思われます。
全体のソースコード
Java初心者が書いたヘタクソなコードですが、何卒ご了承ください。パッケージ名はJavaでLINQを実装した方のパクリですごめんなさい。型名も変更する必要があったりするのでしょうか……?
package com.github.jrper;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
public class ReactiveProperty<T> implements Property<T> {
private Property<T> value;
// コンストラクタ
public ReactiveProperty(){
value = new SimpleObjectProperty<>();
}
public ReactiveProperty(T t){
value = new SimpleObjectProperty<>(t);
}
// Propertyインターフェースを継承するので……
@Override
public void addListener(ChangeListener listener) {value.addListener(listener);}
@Override
public void removeListener(ChangeListener listener) {value.removeListener(listener);}
@Override
public void setValue(Object value){this.value.setValue((T)value);}
@Override
public void bind(ObservableValue observable) {value.bind(observable);}
@Override
public void unbind() {value.unbind();}
@Override
public boolean isBound() {return value.isBound();}
@Override
public void bindBidirectional(Property other) {value.bindBidirectional(other);}
@Override
public void unbindBidirectional(Property other) {value.unbindBidirectional(other);}
@Override
public Object getBean() {return value.getBean();}
@Override
public String getName() {return value.getName();}
@Override
public void addListener(InvalidationListener listener) {value.addListener(listener);}
@Override
public void removeListener(InvalidationListener listener) {value.removeListener(listener);}
// getter
public T getValue(){return value.getValue();}
// ReactivePropertyからReactivePropertyを作成
public <R> ReactiveProperty<R> select(Function<T, R> func){
ReactiveProperty<R> retVal = new ReactiveProperty<>(func.apply(this.getValue()));
this.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply((T)newValue));});
return retVal;
}
// 2つのReactivePropertyからReactivePropertyを作成
public <U, R> ReactiveProperty<R> combineLatest(ReactiveProperty<U> u, BiFunction<T, U, R> func){
ReactiveProperty<R> retVal = new ReactiveProperty<>(func.apply(this.getValue(), u.getValue()));
this.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply((T)newValue, u.getValue()));});
u.addListener((sb, oldValue, newValue)->{retVal.setValue(func.apply(this.getValue(), (U)newValue));});
return retVal;
}
// ◯◯PropertyからReactivePropertyを作成
public static <R> ReactiveProperty<R> ToReactiveProperty(Property<R> r){
ReactiveProperty<R> retVal = new ReactiveProperty<>(r.getValue());
r.addListener((sb, oldValue, newValue)->{retVal.setValue(r.getValue());});
return retVal;
}
// 変更通知機能
public void subscribe(Consumer<T> cons){
this.addListener((sb, oldValue, newValue) -> {cons.accept(this.getValue());});
}
}