Web技術勉強会2011/07/23
Ryuichi TANAKA/@mapserver2007/summer-lights.jp
JavaScriptでプロトタイプベースオブジェクト指向プログラミング
~続々・親子関係を維持してクラスを使わないオブジェクト指向プログラミング手法~
これまでの内容
� JavaScriptのプロトタイプベースOOPライブラリ「mix.js」を開発を開始
� mix.jsの初期バージョン完成
� バグあり。IEで動かない。
� バグ修正版mix.jsをリリース
� バグなし、IEでも動作。� バグなし、IEでも動作。
� mix.js用ライブラリの開発
� 前回一部紹介。
順調に育ってます
� 開発から約2ヶ月。
� mix.js本体:213行
� コメント、改行のみの行を含んでいるので実質190行くらい。
� mix.modules.js:743行
� モジュール群。実質680行くらい。
� Utils、Cache、Cookie、Http、Design� Utils、Cache、Cookie、Http、Design
� アプリケーションにも現在適用中(RankForce3, Rails製のアプリ)。
ライブラリ開発でレベルアップ
� これまで触ったことのない深い知識が必要
� プロトタイプチェーンのつなぎ替え
� クロージャ連発
� 厳密なエラー処理
� テスト
� このライブラリ開発でかなり詳しくなれた、と自負。� このライブラリ開発でかなり詳しくなれた、と自負。
� JavaScriptに関しては中級者以上の実力があると、勝手に自負してるが、このライブラリは我ながらかなり使えると思ってる。なので、これからどんどん使っていく予定。
ライブラリ開発は大変
� 使う側から、使わせる側へ
� IEで動かすことが特に大変
� Chromeでは動くが、IEで動かないことはざら。だが、IEを簡単に切り捨てることは負けを認めたことになる。
� テストがないと本当に死ねる
� テストをつねに通る状態にしておかないと、機能追加や仕様変更� テストをつねに通る状態にしておかないと、機能追加や仕様変更で地獄を見る。
� 通常のアプリと違って、影響範囲がほぼコード全域に及ぶ。したがって、使い捨てのテストコードで都度確認するわけにはいかない。常に過去に正しく動いていて、かつ、変更後も正しく動くことを保証し続けないといけない。
� テストがなかったら途中で投げてたかも。
� おかげで、機能変更がまったく怖くない。
閑話休題
� ここから本題
今回の内容
� まずはこのコードをみてほしい。
� Child.java
package jp.sample;
public class Child extends Parent {
public void caller() {
super.caller();super.caller();
}
public void name() {
System.out.println("name is child");
}
}
今回の内容� Parent.java
package jp.sample;
public class Parent extends GrandParent {
public void caller() {
super.caller();
}}
public void name() {
System.out.println("name is parent");
}
}
今回の内容� Parent.java
package jp.sample;
public class GrandParent {
public void caller() {
name(); // これがどこを指すか?}}
public void name() {
System.out.println("name is grand parent");
}
}
これを実行すると
� Test.java
package jp.sample;
public class Test {
public static void main(String[] args) {
Child child = new Child();
child.caller();
� 問題:
� これを実行すると、何と表示されるでしょう?
child.caller();
}
}
クラス図だとこんな
手順:・Childをインスタンス化・Child#callerを実行する。・Child#callerはParent#calerを呼び出す・Parent#callerはGrandParent#callerを呼び出す・GrapndParent#callerはGrandParent#nameを
呼び出す。
GrandParent#nameはどこを指す?
答え
� 答え。
� だからどうした、Javaじゃねえか!
� はい。でも、なんで「name is grand parent」じゃないのか。なんとなく不思議に思わないかい?
$ > java Test
$ > name is child
のか。なんとなく不思議に思わないかい?
レシーバを意識してみる
child.super()super().caller()
イメージとしてはこんな感じになる。(あくまでイメージ)
child.super().caller()
レシーバは常にchildになるので、GrandParent#callerが呼ばれてもchildとして呼ばれることになる。したがってname()はChild#name。
ちなみにレシーバはRuby用語。
だが、JavaScriptだとこうはならない
� JavaScriptで同じことをやると…
child.super().super().caller()
↓
parent.super().caller()
↓
grandparent.caller()grandparent.caller()
継承をうまいことやってあげたとする。Javaと同じ呼び出し方をしても、親を呼び出した時点でレシーバが親のレシーバに変化してしまう。最終的にはGrandParent#callerはGrandparent#nameを実行する。
※というかこういう継承関係をとれるライブラリがほとんどないわけだが。
call, applyを使うと同じことはできる
� 同じことは実はできる
child.super().super().caller()
↓
super.apply(child)
↓
child.super().caller()child.super().caller()
↓
super.apply(child)
↓
caller.apply(child)
↓
child.caller()
call, applyを使うと同じことはできる
� call, applyは外部のオブジェクトを第一引数のオブジェクトとして呼び出すことができる神メソッド。
� callとapplyの違いは第二引数で渡す引数の形式の違い。
� callは個別に渡す(いくつでも渡せる)
� applyは配列で渡せる(1つだけ)。argumentsをそのまま渡すときはこっちを使う。
� つまり、第一引数のオブジェクトをレシーバとして指定できる。指定しない場合は呼び出し先のオブジェクト。
� これを利用するとさっきJavaでやったことと同じになる。
ここでmix.jsの話にようやく入る� mix.jsでもまったく同じことが起きる
� 回避方法も全く同じで、call, applyを使う
�が、これはない!!!�
� なぜか。継承するたびに、レシーバを子供に戻す作業が毎回発生する。
� つまり、mix.jsを使う開発者に余計な手間をかけさせることになる。
� 普通、Javaなどと同じ挙動になるだろうと思うはず。でそのとおりにならないのでバグになる、と。
� ライブラリとは、こういうことを全部よしなにやってあげるためにあるわけで。故にこれはない。
要するに今回の内容は
� 親メソッドでthisを使っている場合、自動的にcall, applyをラップしてあげるよ、という実装の話。
� 動作イメージ
� var Psp = Module.create({
� name “psp”,
� getName: function() { return this.name; }� getName: function() { return this.name; }
� });
� var PsVita = Module.create({
� name: “psvita”,
� getName: function() { return this.parent.name; }
� });
� var obj = PsVita.mix(Psp);
� obj.getName(); // psvita
とりあえずなにも考えずに実装
� これまでは親を「parent」プロパティで参照していた
� obj.parent.getName();
� parentを挙動を変えて実装してみた。
� が、失敗。
� 常に子をレシーバにして呼び出すことはできたが、既存の処理が正常に動作しなくなった。正常に動作しなくなった。
� 具体的にはhasメソッド。テストが軒並みこけるように。
� 一旦白紙に戻し方針転換。
� いっそ、既存の処理に影響がでないようにプロパティを新たに追加することにした。
parentと__parent__
� あらたに「__parent__」を定義。
� parentは「外部用親参照プロパティ」と定義
� __parent__は「内部用親参照プロパティ」と定義
� parent経由で親を参照するとレシーバは常に子になる
� __parent__経由で親を参照するとレシーバは現在参照しているオブジェクト(モジュール)。いるオブジェクト(モジュール)。
� ルール上、__parent__の仕様は非推奨とした。
� __parent__はあくまで「内部」で使う目的で定義したため
� 「__」をつけているのは通常のプロパティとは違う、という意味を含めている。
� mix.jsの作り上、特定のプロパティを非公開(private)にはできないので、参照自体は可能。
実装の肝
� 実装の肝はどうやってレシーバを子に変換するか。
� parentに継承したモジュールのメソッドをチェーンさせるときに、メソッドをfor-inでバラして、メソッドをフックする。
// これまではだいたいこんな感じfor (var prop in parent) {for (var prop in parent) {
child[prop] = parent[prop];
}
// 今はこんな感じfor (var prop in parent) {
child[prop] = hook(child, parent[prop]);
}
実装の肝
� applyを使ってレシーバを子供にしつつ、処理自体は委譲してあげる
function hook(self, f) {
return funciton() {
f.apply(self, arguments);
};
}
してあげる
この機能によって得られる物
� 同じような画面がある場合、ほとんどの処理を委譲できるためコード量を大幅に削減可能
� 処理が似ているけどちょっとだけ違う場合
� オーバーライドが可能なので加工処理などを行える
� 処理が全く同じ場合
� サブモジュール(サブクラスに相当)にメソッド定義しなくてすむ=そ� サブモジュール(サブクラスに相当)にメソッド定義しなくてすむ=そのままスーパーモジュール(スーパークラスに相当)を「自動的に」コールするため不要
� この機能によって一般的なOOPと同じ利点が得られる
まとめ
� mix.jsに内部親参照用プロパティ「__parent__」、外部親参照用プロパティ「parent」を実装
� コードの重複を排除できるようになった
� オーバーライドを自然にできるようになった
� 今後は
� 実装はほぼ終了� 実装はほぼ終了
� mix.js用モジュールはつくるかもしれない
� Webアプリケーションに適用して評価する
Recommended