Hatena::ブログ(Diary)

flashrod このページをアンテナに追加 RSSフィード

2007-08-15

AS3でイベント駆動型の処理を直列並列のどちらでも同じように書きたいときのやり方 00:18  AS3でイベント駆動型の処理を直列並列のどちらでも同じように書きたいときのやり方を含むブックマーク

AS3/Flex2 を使い始めて約半年 - #生存戦略 、それは - subtechを読んだときはGUIの話かと思ってたので、それはまあそういうことはあるよね、くらいの認識だったのだけど、GUI のイベントスクリプティング - Windchaseを読んで非同期な処理を同期化するとかイベント駆動型の処理をうまく扱うやり方の話だとわかった。なのでAS3で書いてみた。上の記事のruby版とはかなり違うので読み比べる人は注意。

まず最初に「処理」を抽象化するインターフェースはこんなの。

    public interface Job {
        function addEventListener(type:String, fn:Function, useCapture:Boolean = false,
              priority:int = 0, useWeakReference:Boolean = false):void;
        function removeEventListener(type:String, fn:Function, useCapture:Boolean = false):void;
        function start():void;
    }

なんだかちょっといまいちだけど。ここでは「処理」っていうのは

  1. このJobインターフェースをimplementsしたもの
  2. 処理が終わったら 'finished' イベントを発射する

ものと定義することにする。要するに「処理」を Observable なオブジェクトにして「処理が終わった」イベントを新設するわけ。

たとえば、URLを指定してHTTP GET する処理はこんなふうになる。

    import flash.display.Loader;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.net.URLRequest;
    public class URLGet extends EventDispatcher implements Job {
        private var url:String;
        public function URLGet(url:String) { this.url = url; }
        public function start():void {
            var loader:Loader = new Loader();
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e:Event):void {
                trace("url get succeeded", url);
                dispatchEvent(new Event("finished"));
            });
            loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent):void {
                trace("url get failed", url);
                dispatchEvent(new Event("finished"));        
            });
            trace("url get start", url);
            loader.load(new URLRequest(url));
        }
    }

読み込みに成功したときも失敗したときも、'finished'イベントを発射する。Jobインターフェースの最初の二つのメソッドは EventDispatcher のそれと一致させてあるのでEventDispatcher 派生にしておけば実装は任せておける。

他の例としては、指定したミリ秒待つだけの処理はこんなふうに、指定された時間待った後'finished'イベントを発射するだけ。

    import flash.events.EventDispatcher;
    import flash.events.Event;
    import flash.utils.setTimeout;
    public class Wait extends EventDispatcher implements Job {
        private var time:int;
        public function Wait(time:int) { this.time = time; }
        public function start():void {
            setTimeout(function():void {
                dispatchEvent(new Event('finished'));
            }, time);
        }
    }

もうひとつ、単にデバッグトレースするだけの処理。

    import flash.events.EventDispatcher;
    import flash.events.Event;
    public class Trace extends EventDispatcher implements Job {
        private var s:String;
        public function Trace(s:String) { this.s = s; }
        public function start():void {
            trace(s);
            dispatchEvent(new Event('finished'));
        }
    }

さて本題の並列化と直列化だけど、まずは直列化。

    import flash.events.EventDispatcher;
    import flash.events.Event;
    public class Sequence extends EventDispatcher implements Job {
        private var jobs:Array;
        public function Sequence(...args) { this.jobs = args; }
        public function start():void {
            if (jobs.length > 0) {
                var job:Job = jobs.shift();
                job.addEventListener('finished', onFinished);
                job.start();
            } else {
                dispatchEvent(new Event('finished'));
            }
        }
        private function onFinished(e:Event):void {
            var job:Job = e.target as Job;
            job.removeEventListener('finished', onFinished);
            start();
        }
    }

一個ずつ順番に処理して全部終わると直列化の終わりとして'finished'イベントを発射する。ここだけ見てもstartとonFinishedの関係がちょっとスパゲティぽく感じる人は居るかもしれない。

次に並列化

    import flash.events.EventDispatcher;
    import flash.events.Event;
    public class Concurrent extends EventDispatcher implements Job {
        private var jobs:Array;
        public function Concurrent(...args) { this.jobs = args; }
        public function start():void {
            for each (var job:Job in jobs) {
                job.addEventListener('finished', onFinished);
            }
            for each (job in jobs) {
                job.start();
            }
        }
        private function onFinished(e:Event):void {
            var job:Job = e.target as Job;
            job.removeEventListener('finished', onFinished);
            jobs.splice(jobs.indexOf(job), 1);
            if (jobs.length == 0) {
                dispatchEvent(new Event('finished'));
            }
        }
    }

同時に全部開始して、全部終わったら並列化の終わりとして'finished'イベントを発射する。

Sequenceと Concurrentは要するにJobのコンポジションなのでネストできる。使い方はこんなかんじで

            var job:Job = new Sequence(new Trace("begin"),
                                     new Concurrent(new URLGet("http://www.google.com/"),
                                                    new URLGet("http://www.yahoo.com/"),
                                                    new URLGet("http://www.apple.com/"),
                                                    new URLGet("http://www.mozilla.org/")),
                                     new Sequence(new URLGet("http://www.cnn.com/"),
                                                  new URLGet("http://del.icio.us/")),
                                     new Trace("wait for 2 sec."),
                                     new Wait(2000),
                                     new Concurrent(new URLGet("http://tumblr.com/"),
                                                    new URLGet("http://flickr.com/")),
                                     new Trace("end"));
            job.start();

実行するとこんなふうになる。

begin
url get start http://www.google.com/
url get start http://www.yahoo.com/
url get start http://www.apple.com/
url get start http://www.mozilla.org/
url get succeeded http://www.google.com/
url get succeeded http://www.yahoo.com/
url get succeeded http://www.apple.com/
url get succeeded http://www.mozilla.org/
url get start http://www.cnn.com/
url get succeeded http://www.cnn.com/
url get start http://del.icio.us/
url get succeeded http://del.icio.us/
wait for 2 sec.
url get start http://tumblr.com/
url get start http://flickr.com/
url get succeeded http://tumblr.com/
url get succeeded http://flickr.com/
end

ちゃんと期待通りだ。

実際にまっとうなライブラリにする場合は、Jobインターフェースをimplementsだと面倒くさそうなのでクロージャを渡す仕様にしたほうが良いような気がする。

以上はflashのイベント処理をそのまま利用するやりかたなわけだけど、GUI のイベントスクリプティング - Windchaseでやっているように、自前でイベントループを持つやりかたはもっと広い使い道があるように思う。

AS3だとこんなふうにルートクラスでメッセージポンプふうのものを作ってしまって

       public class Root extends Sprite {
            private static var queue:Array = [];
            public function Root() {
                addEventListener(Event.ENTER_FRAME, function(e:Event):void {
                    if (queue.length > 0) {
                        queue.shift()();
                    }
                });
            }
            public static function enqueue(fn:Function):void {
                queue.push(fn);
            }
       }

こうすると Root.enqueue(function():{〜}) で任意のクロージャを「できるだけすぐ後でやる」ようにどこからでも登録できる。この「すぐ後でやる」というのはGUIプログラミングでは結構いろんなところで役立つので、小さいアプリでも作っておくと吉かもしれない。大規模システムでMQみたいなのが必要なのと似ている。

追記1

とある所で Sequence でstartしたjobがGCされるんじゃね?と指摘されてるよと教えられて、確かにそういうことはあるかもね、と気づいてしまったので直してみる。こういうバグ改修は理想的には実際にバグの発現を観測してからにしたいところだけど、再現させるほうが難しいな。

    import flash.events.EventDispatcher;
    import flash.events.Event;
    public class Sequence extends EventDispatcher implements Job {
        private var jobs:Array;
        public function Sequence(...args) { this.jobs = args; }
        public function start():void {
            if (jobs.length > 0) {
                var job:Job = jobs.shift();
                job.addEventListener('finished', function(e:Event):void {
                    job.removeEventListener('finished', arguments.callee);
                    start();
                });
                job.start();
            } else {
                dispatchEvent(new Event('finished'));
            }
        }
    }

これでいいかな?だめかもしれないけど。あぁ、前よりすっきりしてしまってなんだか悔しい。

あといろんなところでエラーチェックとかしてないので本気で使う人はそれなりに実装してください。

追記2

'finished'カスタムイベントじゃなくて Event.COMPLETE で良いのでは、という指摘も別にうけたのだけど、確かにそのほうが良いような気もするし、あえて使いまわさないほうが良いような気もする。これは正直どっちが良いか良くわからない。

名無しさん名無しさん 2007/08/17 02:10 ページビュー123460、おしい!参考になるよ

Connection: close