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 では逆順に追加するようになっているようですが、特に気にしないことにします。