prototype.js の Class 拡張試案 (3): Mixin

Update: 末尾に追記があります

id:reinyannyan:20051028:p1 で、Class.Methods というものを導入してみました。全てのオブジェクトでデフォルトのメソッドとして使えたら便利、というものをまとめておき、インスタンス生成時に自動的に組み込まれるように仕掛けておく、というものです。

var Class =
{
    ...
    concrete: function ()
    {
	var klass = function ()
	{
	    Object.extend(this, Class.Methods);	    // <- ココ
	    Class.initialize(this, null, arguments);
	};
	klass.extend = this.extend;
	return klass;
    },
    ...
};

これ自体が Mixin 的と言えますね。この場合 Class.Methods が mix-in されるオブジェクト (Ruby 的に言うとモジュール) です。

今回の話はこれとは別で、同様のことをクラス定義の中でダイナミックに行えないか、ということを考えます。

(なお、上のコード例で initialize メソッドを呼び出す前に Class.Methods を埋め込んでいることがポイントとなりますので、ご留意ください。)

こういうことが出来るようになりたいわけです:

var MyModule =
{
    super_useful_method: function ()
    {
	//
    }
};

var MyClass = Class.concrete();
MyClass.prototype =
{
    initialize: function ()
    {
	this.include(MyModule);
	this.operation();
    },
    operation: function ()
    {
	// 自分のメソッドのとして使える
	this.super_useful_method();
    }
};

自然な流れとして、Class.Methods の中に求めるメソッド (include) を定義することにします。

さて、include が行うべきこととは何でしょうか? それは、オブジェクトの拡張に他ならないわけなんですね。最初の例で既に答えが出ていますが、Objec.extend を使って実装できます:

Class.Methods =
{
    include: function (object)
    {
	return Object.extend(this, object);
    },
    ...
};

ここで、ひと捻りです。

上の include は Ruby の同名のメソッドの実装で、クラスを拡張 (モジュールを追加) するためのものです。これに対し、Ruby には extend というメソッドもあります。これは、下のように、オブジェクト (インスタンス) を拡張する、というものです。

obj.extend(module)

という風に使うんですが、上の JavaScript 版の include、これは結局インスタンス・メソッドですから、Ruby の extend と同様の使い方が出来てしまうんですね:

var o = new MyClass;
o.include(OtherModule);
o.method_in_OtherModule();  // O.K.

はい。ややこしいので、Ruby (prototype.js の文化的バックグラウンド) に合わせる意味で、クラスを拡張する場合は include、オブジェクトを拡張する場合は extend として使い分けられるようにしてみましょう:

Class.Methods =
{
    extend: function (object)
    {
	return Object.extend(this, object);
    },
    ...
};
Class.Methods.include = Class.Methods.extend;	// エイリアス

以上で mixin の仕組みは出来上がりです。結局 extend に別名を付けただけ、という感じですが、枠組みとしての整理は出来たんではないかと思います。

追記 [051224]: Ruby では include と extend の挙動が違うことに気付いたので、それに合わせてみます。

include:

Class.append_features = function (object, module)
{
    for (var prop in module)
	(function (prop)
	 {
	    if ((typeof module[prop]) == 'function')
		object[prop] = function ()
		{
		    return module[prop].apply(object, arguments);
		};
	    else
		object[prop] = module[prop];
	 })(prop);
};
Class.Methods.include = function ()
{
    for (var i = 0; i < arguments.length; i++)
	Class.append_features(this, arguments[i]);
    return this;
};

メソッドの呼び出しを代理呼び出しとすることで、実行時のモジュール (ここではただのオブジェクトのことを言っています)・メソッドの変更が、全ての include 先クラスで反映されるようになります。元の実装だと、include されたメソッドは (include 時点での) オリジナルのメソッドへの参照を保持するので、下手をすると幾つものメソッド定義がメモリー上に残り続けることになります。

extend:

Class.Methods.extend = function ()
{
    for (var i = 0; i < arguments.length; i++)
	Object.extend(this, arguments[i]);
    return this;
};

include とともに、複数の引数を取れるようにしてみました。1.7 以降の Ruby では逆順に追加するようになっているようですが、特に気にしないことにします。