この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

Overlay Typesを使う

GWTに限った話ではありませんが、JavascriptJavaへの橋渡しにはJSONを使うのが一般的です。
で、Overlay Typesを使うことで結構らくちんできました、というお話。

今日のお題

はい、では今日のお題。


例えば複数のチェックボックスを表示する場合なんですが、
UiBinderを使うとこんな感じになると思います。

(ui.xml)

<g:CheckBox ui:name="test" ui:formValue="1" ui:field="foo">FOO</g:CheckBox>
<g:CheckBox ui:name="test" ui:formValue="2" ui:field="bar">BAR</g:CheckBox>
<g:CheckBox ui:name="test" ui:formValue="3" ui:field="baz">BAZ</g:CheckBox>


(Java)

public class HogePanel extends Composite{

    @UiField
    CheckBox foo;

    @UiField
    CheckBox bar;

    @UiField
    CheckBox baz;

    ...(以下略)
}

。。。もう何だかこの時点でメンドクサイ。と思いません?
3つならまだいいけど、チェックボックスが10個もあった日にゃそれはもう。


というわけで、複数のチェックボックスを一つのウィジェットとして扱うための
コンポーネントを作ってみます。

普通に書くと

まあ、こんな感じになると思います。

public class CheckBoxGroup
    extends Composite
    implements HasText, HasName {

    /** 以下の2つのプロパティのsetter/getterは省略 */
    private String name;
    private String text;

    private final Panel basePanel = new VerticalPanel();

    public CheckBoxGroup() {
        this.initWidget(this.basePanel);
    }

    public void onAttach() {
        this.basePanel.clear();
        this.addCheckBoxes();

        super.onAttach();
    }

    private void addCheckBoxes() {
        final Panel layoutPanel = new HorizontalPanel();
        final JSONArray entries = (JSONArray) JSONParser.parseStrict(this.text);

        for (int i = 0; i < entries.size(); i++) {
            layoutPanel.add(this.createCheckBox((JSONObject) entries.get(i)));
        }

        this.basePanel.add(layoutPanel);
    }

    private CheckBox createCheckBox(final JSONObject entry) {
        final CheckBox checkBox = new CheckBox();
        checkBox.setName(this.name);
        checkBox.setText(((JSONString) entry.get("text")).stringValue());
        checkBox.setFormValue(((JSONString) entry.get("formValue")).stringValue());

        return checkBox;
    }
}

(※実はコレ、試してません。たぶんベタに書くとこんな感じになるのでは、と。。。)


で、上記のようなコンポーネントを作ることで、
ui.xmlでこのように書けるようになるわけです。

<g:Label>好きなモビルスーツ</g:Label>
<r:CheckBoxGroup ui:name="test" ui:field="mobilesuits">
[
    { text: 'ガンダム', formValue: 'RX-78' }
    , { text: 'ザク', formValue: 'MS-06F' }
    , { text: 'アッガイ', formValue: 'MSM-04' }
]
</r:CheckBoxGroup>

xml名前空間は適当)



public class HogePanel extends Composite{

    @UiField
    CheckBoxGroup mobilesuits;

    ...(以下略)
}


結構スッキリ書けるようになりました、が。。。
しかし、コレはコレで別の問題が発生しています。

何がアレかというと

JSONArrayやJSONObjectはタイプセーフでないということ。
だから、

final JSONArray entries = (JSONArray) JSONParser.parseStrict(this.text);

とか

checkBox.setText(((JSONString) entry.get("text")).stringValue());
checkBox.setFormValue(((JSONString) entry.get("formValue")).stringValue());

とか、イヤ〜な感じのコードを書くことが強制されてしまいます。
今時、Generics使えないとかありえないっス。


Java5が発表されて以降、Genericsにどっぷりクビまで浸かったワタクシとしては、
キャストを使うなんて有り得ません。全部タイプセーフじゃなきゃヤダ!

      _, ,_
     (`Д´ ∩ < ヤダヤダ!!
     ⊂   (
       ヽ∩ つ  ジタバタ
         〃〃

そこでOverlay Typesの登場

GWTのOverlay Typesとは要するに、
JavaScriptのObjectを『見かけ上、POJOとして扱うようにする』ための仕組みです。
(※overlay→うわべ、外面という意味)


だから、

var something = {
    foo: 1, 
    bar: 'abc'
};

なんて値が存在したとき、これをムリヤリ

public class Something {
    public int getFoo() { ... }
    public String getBar() { ... }
}

で扱えるようにしてしまう、というモノ。


使い方は簡単で、以下のルールを守ったPOJOクラスを作ればいいだけ。


例えば上記のSomethingクラスは、真面目に書くと次のようになります。
ただし、

  • OverlayTypesはあくまでも見せかけだけの機能である
  • GWTは、実際にはJavascriptにコンパイルされてクライアントサイドで動作する

なので、実際のJavascriptの値にアクセスするには次のようにJSNIを使う必要があります。

public class Something extends JavaScriptObject {

    protected Something() {}

    /** ファクトリメソッド */
    public static native Something parse(String json)/*-{ return eval(json); }*/;

    public final native int getFoo() /*-{ return this.foo; }-*/;
    public final native String getBar() /*-{ return this.bar; }-*/;
}

Overlay Typesを使うと

先程のCheckBoxGroupは、こんな感じにリファクタリング出来るようになるわけです。

public class CheckBoxGroup
    extends Composite
    implements HasText, HasName {

    // ...ここまで全く同一なので省略

    private void addCheckBoxes() {
        final Panel layoutPanel = new HorizontalPanel();
        final JsArray<CheckBoxEntry> entries = CheckBoxEntry.parse(text);

        for (int i = 0; i < entries.length(); i++) {
            layoutPanel.add(this.createCheckBox(entries.get(i)));
        }

        this.basePanel.add(layoutPanel);
    }

    private CheckBox createCheckBox(final CheckBoxEntry entry) {
        final CheckBox checkBox = new CheckBox();
        checkBox.setName(this.name);
        checkBox.setText(entry.getText());
        checkBox.setFormValue(entry.getFormValue());

        return checkBox;
    }

    public static class CheckBoxEntry extends JavaScriptObject {
        protected CheckBoxEntry() {}

        public final native String getText()/*-{ return this.text; }-*/;
        public final native String getFormValue()/*-{ return this.formValue; }-*/;

        public static native JsArray<CheckBoxEntry> parse(final String json)/*-{
            return eval(json);
        }-*/;
    }
}

キャストが一つも存在しない美しい世界に。スバラシイ!


。。。ま、結局のところ見せかけだけなので、実行時エラーは避けられないんですけどね。