mxmlc コンパイラのメタデータタグを利用する - [Bindable] 篇

ActionScript3 のソースをコンパイルするとき、現在は flex2 SDK に含まれる mxmlc を使うのが主流です。このコンパイラはAS3の言語では定義されてない、flex 独自のメタデータタグ([] で囲まれた syntax。[Bindable] など)を解釈し、自動でAS3ソースコードをジェネレートしてくれます。
このメタデータタグの利用方法をきちんと把握すると、だいぶコード記述が減り、シンプルかつ統一的なAS3(flex)プログラミングが可能になり、プログラミングの生産性が向上するであろう、有益な機能です。しかしながら、どういった挙動をするソースが生成されるのか、どのようなメタデータが記述可能なのか、というのがいまいち解っていないため、エントリーとして書き起こします。
[Bindable] メタデータを使うといったい何が嬉しいのでしょうか。その前に Flash でのプログラミングの話です。
Flash ではイベント駆動のプログラミングが基本です。処理があるとなんらかのイベントを投げて、そのイベントを待ち受けしているオブジェクトが処理します。非常に単調な処理ではイベントを投げまくればなんとなく処理ができてしまうため、頭は使いません。しかし複数の描画オブジェクト/オブジェクトの状態/ネットワーク通信などなどがたくさん絡んでくると、イベントを受け取っては投げ、受け取っては投げ、ということを行うため非常に混乱しがちです。きちんと設計をしなくては破綻します*1。そこの一つの解決方法として使えるのが、[Bindable] のメタデータです。
突然ですが、次のソースは、x, y の位置に半径 radius で、col/secCol 色の花を描画する Flower クラスの一部抜粋です。

   public class Flower extends Sprite {
        public var radius:int;
        public var col:uint;
        public var secCol:uint;

        public function Flower(x:int, y:int, radius:int, col:uint, secCol:uint) {
            this.x = x;
            this.y = y;
            this.radius = radius;
            this.col = col;
            this.secCol = secCol;
        }

        public function draw():void {
〜実装〜
        }
    }

このクラスを用いて花を描画するには

var flower:Flower = new Flower(100,100, 50, 0, 0xFF0000);
flower.draw();

として使います。では花の色を変更してみましょう。

flower.col = 0x0000FF;
flower.draw();

色を変えてから再描画することで、見た目を変化させています。しかしながらこれでは色を変えたあと、毎回毎回 draw() 関数を呼び出さなければならず、面倒です。そこで普段は setter を定義して、setter 内部で draw() メソッドを呼び出します。

public var radius:int;
private var _col:uint;
public var secCol:uint;

public function get col():uint {
    return _col;
}

public function set col(newCol:uint):void {
    _col = newCol;
    draw();
}

このように変更することで、意図した動作をするようになります。では次に secCol も変更したときに色が変わるような実装にしてみましょう。

public var radius:int;
private var _col:uint;
private var _secCol:uint;

public function get col():uint {
    return _col;
}

public function set col(newCol:uint):void {
    _col = newCol;
    draw();
}

public function get secCol():uint {
    return _secCol;
}

public function set secCol(newCol:uint):void {
    _secCol = newCol;
    draw();
}

だんだんと冗長になってきましたね。次に radius…などなど、今回の例ではプロパティが三つなのでいいですが、数がどんどん多くなっていくと、めんどくさくてやってられないですね。ただの setter/getter といえど行数が増えるとバグも混入しやすくなります。
そこで [Bindable] メタデータタグの出番です。[Bindable] の次に書いたプロパティにはコンパイル時に自動的に setter/getter が設定されます。そして、そのデータが変更された場合、デフォルトで PropertyChangeEvent.PROPERTY_CHANGE イベントが投げられます。つまり、今までのはこのように書くことができるのです。

public var radius:int;
[Bindable]
public var col:uint;
[Bindable]
public var secCol:uint;

public function Flower(x:int, y:int, radius:int, col:uint, secCol:uint ) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.col = col;
    this.secCol = secCol;

    addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, propertyChangeHandler);
}

private function propertyChangeHandler(e:PropertyChangeEvent):void {
    draw();
}

おお、すっきり!コンストラクタで自分自身のイベントリスナに PropertyChangeEvent.PROPERTY_CHANGE を登録してやるだけで、[Bindable] で指定したプロパティが変更された通知を受け取ることが可能なのです。いったい何でこんな事になるのか意味不明だと気持ち悪いので、生成されたソースを見てみましょう。mxmlcコンパイラオプションで -keep をつけると、 generated ディレクトリ以下に生成されたソースが残ったままになります。

class BindableProperty
{
    /**
     * generated bindable wrapper for property col (public)
     * - generated setter
     * - generated getter
     * - original public var 'col' moved to '_98688col'
     */

    [Bindable(event="propertyChange")]
    public function get col():uint
    {
        return this._98688col;
    }

    public function set col(value:uint):void
    {
        var oldValue:Object = this._98688col;
        if (oldValue !== value)
        {
            this._98688col = value;
            dispatchEvent(mx.events.PropertyChangeEvent.createUpdateEvent(this, "col", oldValue, value));
        }
    }
〜続く〜

というわけで、setter/getter の定義が生成されてますね。setter の方ではセットし終わった後に、dispatchEvent で mx.events.PropertyChangeEvent.createUpdateEvent で mx.events.PropertyChangeEvent を作って投げてます。PropertyeChangeEvent は変更に関する様々な情報を持っているため、たとえば値が10以上変わったときのみに変更、などの処理が簡単にハンドラ内部で行えます。また、イベントとして呼ばれるため、外部のクラスが Flower インスタンスの状態を監視し、たとえば花の色のモニタリングを別の箇所で、といったようなイベント処理も簡単に行えますね。
そのような処理では mx.binding.utils.ChangeWatcher を使うと、簡単に Bindable で指定した値の監視を行えます。

ChangeWatcher.watch(flower, 'col', function(e:PropertyChangeEvent):void {
     log(e);
});

flower.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, ...) じゃだめなの?という声はもっともですが、ChangeWatcher では簡単に監視イベントリスナの削除や、プロパティチェインの監視などなどが行えるので、使い分ける感じですね。

その他、Bindable メタデータタグはクラス自体に定義することもできます。すべてのプロパティの変更を監視しておきたい時に便利でしょう。

[Bindable]
public class Flower extends Sprite {
    public var radius:int;
    public var col:uint;
    public var secCol:uint;

これは

public class Flower extends Sprite {
    [Bindable]
    public var radius:int;
    [Bindable]
    public var col:uint;
    [Bindable]
    public var secCol:uint;

と同じです。
今回は Bindable メタデータタグの使い方について触れました。mxmlc に含まれる機能を積極的に使っていくとだいぶ楽にコードが記述できそうです。当初は Flex フレームワークを使うだけで UIComponent などなどの重量級のデータも加わって swf が肥大化するんじゃ、と思ったのですが、必要な部分だけを含めてくれるらしく、大して swf は大きくなりませんでした。というわけでどんどん利用していこうと思います。

*1:まだ自分は慣れてないので破綻しまくります