Command Pattern

([20060226]: RSS リーダーの方にはご迷惑かもしれませんが、読み返してみて自分でもちょっと理解し辛かったので、ほんの少しだけ加筆・修正しました)

先日の HTTP 通信の話 (id:reinyannyan:20060131:p1) の続きです。パターンの解説ではありません。パターン適用の一例としてお読みください。

上の記事で述べたように、JavaScript で様々な web 上のメンテナンス作業を行うなかで、大量の HTTP リクエストが逐次的に実行されるような仕組みを必要としていたわけなんです。そしてたどり着いたのが「コマンド・パターン」というものでした。

「コマンド・パターン」とは、実装してみると分かりますが、クロージャのオブジェクト版といった感じです。メソッドとそれへのパラメータを一体的にオブジェクト化することで、他のオブジェクトに投げたり、蓄積して呼び出しを遅延したり、といったことが容易に出来るようになります。

状況

このパターンが求めていたものだと思いはしたものの、以下のようなシンプルなリクエストの仕方が通るように実装したい、というのが私の要求でした。

(new XMLHTTP).get(url, on_complete);
var http = new XMLHTTP;
urllist.each(function (url)
{
    http.get(url, on_complete);
});

つまり、それが一度きりのリクエストなのか、一連のリクエストの始まりなのか、あるいは途中なのか終わりなのか、ということを利用者 (プログラマ) が全く意識する必要無しに、リクエストがコマンド・オブジェクトとして自動的にプールされ、逐次的に実行されるようにしたかったわけです。

問題

上の条件から、解決すべき問題は次のようなものです。

コマンドのシステムを XMLHTTP オブジェクトの利用者の視界から隠したいわけですから、コマンドの真の利用者 (以下「クライアント」) は必然的に XMLHTTP オブジェクト、ということになります。しかし、XMLHTTP オブジェクトからも外の世界は見えません。即ち、リクエストがまだ後に来るのか、これで最後なのか、といった状況が全く分からないまま、コマンド開始等の操作をしなければならない、ということです。

解決

仮に、リクエストが一回しか来なかった場合、クライアントは、作成したコマンド・オブジェクトをすぐさま実行することが望まれます。

また、複数回来る場合を想定しても、最終を示す合図は待っていても来ないわけですから、やはり最初のリクエストが来た時点で実行すべき、ということになります。

このことを、クライアント側の便も考慮して、コマンドの登録と実行のコールは同時に行う、それを受け取り側で一旦留保して、最初のリクエストであれば実行、そうでなければプール、という具合に実装すれば良いのではないかと考えられます。

実装

以下、コーディングは私の prototype.js の拡張がベースになっています。なお、明記していなくても実際のコードからの抜粋ですので、省略されている部分があることをお断りしておきます。

Command.Center

コマンドの登録を受け付け、プール、実行するセンターです。

先の記事を書いた時点ではそうではなかったんですが、singleton クラスとして実装しています。

この結果、センターのインスタンス作成から呼び出しまで全てをコマンド・オブジェクトに行わせることが可能となったため、副作用としてセンターがクライアントの視界から隠されることになりました (クライアントのすべきことが簡略化されたわけです)。

また、クライアントとセンターが一対一対応で無くなったことから、センターは自動的にクライアント毎に担当のマネジャーを割り当て、それぞれにコマンド処理を任せるという仕組みになっています (センターは単なる中継役、ということです)。

Command.Center =
{
    Instance: function ()
    {
	if (!this.instance)
	{
	    var center = Class.concrete();
	    center.prototype =
	    {
		initialize: function ()
		{
		    this.list = [];
		},

		assign_manager: function (client)
		{
		    var found, manager;
		    found = this.list.find(function (item)
		    {
			return (item.client == client);
		    });

		    if (found)
			manager = found.manager;
		    else
		    {
			manager = new Command.Center.Manager(this, client);
			this.list.push({ client: client, manager: manager });
		    }
		    return manager;
		},
		sack_manager: function (client)
		{
		    this.list = this.list.reject(function (item)
		    {
			return (item.client == client);
		    });
		},

		accept: function (client, comd, start)
		{
		    this.assign_manager(client).add_command(comd, start);
		},
		reject: function (client, comd)
		{
		    this.assign_manager(client).del_command(comd);
		},
		execute: function (client)
		{
		    this.assign_manager(client).exec_command();
		},
		terminate: function (client)
		{
		    this.sack_manager(client);
		}
	    };
	    this.instance = new center;
	}
	return this.instance;
    }
};
Command.Center.Manager

実際にコマンド処理を担当するオブジェクトです。

コマンドの登録と共に、開始の合図が不必要に何度も送られてくることを想定し (『解決』の節参照)、実行中のフラグを立て、合図を無視できるようにしています。

また、コマンドのタイプが同期か非同期かを問い合わせて、コマンドの終了処理をその場で行わせるかどうか判断しています (exec_command 内)。この終了処理が次のコマンド開始のためのトリガーとなります。

Command.Center.Manager = Class.concrete();
Command.Center.Manager.prototype =
{
    initialize: function (center, client)
    {
	this.center = center;
	this.client = client;
	this.list   = [];
	this.__at_work = 0;
    },

    start: function ()
    {
	if (!this.__at_work)
	    this.exec_command();
    },

    add_command: function (comd, start)
    {
	if (!comd.kind_of(Command.Abstract))
	    return;

	this.list.push(comd);
	if (start)
	    this.start();
    },
    del_command: function (comd)
    {
	this.list = this.list.reject(function (item)
	{
	    return (item == comd);
	});
	if (this.list.empty())
	    this.exec_finalizer();
    },
    exec_command: function ()
    {
	var comd = this.get_command();
	if (comd)
	{
	    this.__at_work++;
	    comd.execute();
	    if (!comd.notify_when_done())
		comd.done();
	}
	else
	    this.exec_finalizer();
    },
    get_command: function ()
    {
	return this.list.shift();
    },

    exec_finalizer: function ()
    {
	this.center.terminate(this.client);
    }
};
Command.Abstract

コマンド・オブジェクトの実装です。汎用性のために、まず抽象クラスから作っています。

Command.Abstract = Class.abstract();
Command.Abstract.prototype =
{
    initialize: function (client)
    {
	this.client = client;
	this.center = Command.Center.Instance();
	this.__async= false;
    },
    execute: Prototype.emptyFunction,

    register: function (start)
    {
	this.center.accept(this.client, this, start);
    },
    unregister: function ()
    {
	this.center.reject(this.client, this);
    },
    done: function ()
    {
	this.center.execute(this.client);
    },

    notify_when_done: function (notify)
    {
	return (this.__async = notify || this.__async);
    }
};
Command.HTTPReq

具体的なコマンド・オブジェクトの実装です。

Command.HTTPReq = Class.concrete();
Command.HTTPReq.prototype = Command.Abstract.extend(
{
    initialize: function (client, method, url, options)
    {
	this.SUPER(client);
	this.method = method;
	this.url    = url;
	this.options= options;
	this.__async= true;
    },
    execute: function ()
    {
	var http = new XMLHTTP.Req(this.method, Ajax.getTransport());
	http.notify_when_done(this);
	http.act(this.url, this.options);
    }
});
XMLHTTP

コマンド・オブジェクト (Command.HTTPReq) のクライアントであり、HTTP リクエストのインターフェースです。

ここで、コマンド・オブジェクトの作成、登録および開始が行われるわけです (comdify)。

このクラスが提供する get 等の HTTP メソッドは、"comdify" を呼ぶためだけのもので、HTTP リクエストの実装部分は独立したクラスとして分けてあります (XMLHTTP.Req <- Command.HTTPReq により実体化される)。

関連する部分のみ抜粋します。

var XMLHTTP = Class.concrete();
XMLHTTP.prototype =
{
    initialize: function ()
    {
	XMLHTTP.Req.METHODS.each((function (method)
	{
	    var METHOD = method.toUpperCase();
	    this[method] = function (url, on_complete, options)
	    {
		this.comdify(METHOD, url, on_complete, options);
	    };
	}).bind(this));
    },
    comdify: function (METHOD, url, on_complete, options)
    {
	var comd = new Command.HTTPReq(this, METHOD, url,
					this.setopts(options, on_complete));
	comd.register(true);
    },
XMLHTTP.Req

HTTP リクエストの実装クラスです。

HTTP 通信の完了とコマンドの完了とを連動させるために、コマンド・オブジェクトから完了通知の代行の依頼を受ける、という形を取っています。

これも関係するメソッドのみ抜粋します。

XMLHTTP.Req = Class.concrete();
XMLHTTP.Req.prototype =
{
    ...
    notify_when_done: function (comd)
    {
	this.command = comd;
    },
    done: function ()
    {
	if (this.command)
	    this.command.done();
    },

結論

結果として、当初の目的が達成され、単純で利用しやすいジョブのプール・実行システムが出来上がったのではないかと思います。

HTTP 通信のために導入したものではありましたが、Command.Abstract のサブクラスであればどれも同じ要領で利用できるので、非常に応用が利くのではないでしょうか。

特に、メソッドとオブジェクトのペア (および引数) を受け取って実行するだけのコマンド・クラスを作れば、どんなオブジェクトも (新たなコマンド・クラスを作ることなしに) このシステムに参加可能だということを指摘しておきたいと思います。

ただ、クライアントがコマンド・オブジェクトを作成・登録するだけで良い (コマンド・センターに対する操作を一切しなくて済む、あるいは出来ない) という設計にしたことがメリットなのか、デメリットなのかは正直まだよく分かっていません。この点は今後の運用次第で見直す必要が出てくるかもしれません。


参考文献:
http://en.wikipedia.org/wiki/Command_pattern
http://www.dofactory.com/Patterns/PatternCommand.aspx
http://www.javaworld.com/javaworld/javatips/jw-javatip68.html