1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaでもReactivePropertyしたいだけの人生だった

Posted at

概要

 少し前に、「JavaFXで十分Data Bindingできるじゃん!」といった記事を書きました。
  Java+JavaFX+MVVMでストップウォッチアプリを作成してみた所感
 ただ、これじゃ足りない!足りないんだよ! 具体的にはジェネリクスで書きたいんだよ!
 StringPropertyだのBooleanPropertyだの、お前それ実装してて一ミリもクソだと思わなかったのかとJava開発者(SunかOracle)を小一時間問い詰めたい命名がされているクラスを眺めていて思いました。
 「じゃあジェネリクスの練習がてらReactiveProperty作ろう」と。

※最終的なコードは文末に貼ってあります

ベースを何にするか

 とりあえず、Property<T>型のインスタンスを内に持ちながら、インターフェースProperty<T>を継承することにしました。実体は……Property<T>を継承しているSimpleObjectProperty<T>でいいでしょう、多分。

sample1.java
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っぽいメソッドを実装しよう

 一応代替物を目指しているので、SelectSubscribeなどの主要なメソッドは実装しておきたいものです。通知方法ですが、内部のインスタンスがaddListenerメソッドを持っているので、そちらに追記することにしました。これだけで、ちゃんと通知機能が働いてくれるのは驚きですね。
 ただ、Javaには「拡張メソッド」の概念が存在しませんので、Selectした結果がいきなりReactiveProperty<T>になりますし、ToReactivePropertyStringPropertyなどをReactiveProperty<T>に置き換えるためだけのメソッドになりました。

sample2.java
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を実装した方のパクリですごめんなさい。型名も変更する必要があったりするのでしょうか……?

ReactiveProperty.java
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());});
    }
}
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?