Hatena::ブログ(Diary)

naoyaのはてなダイアリー

August 13, 2005

prototype.js でデザインパターン - Iterator

Ruby on Rails や Catalyst のプラグインなんかでは prototype.js という JavaScript のライブラリを使って、Ajax サポートを実現しています。prototype.js とフレームワークが必要な Ajax の JavaScript コードを吐き出してくれるので、Ruby プログラマや Perl プログラマは JavaScript の実装を意識しなくても Ajax なインタフェースが作れる、という風になっています。

こんな感じで prototype.js は Ajax な部分に注目が集まっていますが、ほかにも "Class-style OO" なフレームワークも内包してます。

JavaScript はプロトタイプベースのオブジェクト指向言語で、C++ や Java のようなクラスベースのオブジェクト指向言語とはちょっと実装が異なります。プロトタイプベースのオブジェクト指向...は僕も理解がちょっとあいまいなので他のサイトから引用します。

オブジェクトがスロット(クラスのインスタンスならインスタンス変数やメソッドに相当)の追加をクラスに依存せずに自由にできることを前提としたオブジェクト指向。あるいはそうしたオブジェクトを用いたプログラミングや、それをサポートする機構。

プロトタイプベース・オブジェクト指向

つまり、クラスベースな OO では、クラスにインスタンスの性質(プロパティ)や振る舞い(メソッド)が定義されていて、そのクラスを雛形にオブジェクトを生成する、という感じですが、プロトタイプベース OO ではまず先に実体としてのプロトタイプがあって、そこに性質や振る舞いを付け加えていくことでオブジェクトが形成されていく、という感じです。

そんなわけで JavaScript の OO は通常、

/* コンストラクタ */
function Dog (name) {
    this.name = name;
}

/* Dog のメソッド */
Dog.prototype.bark = function () {
    alert(this.name + ': わんわん');
}

dog = new Dog('しなもん');
dog.bark();

みたいな感じで実装します。JavaScript では関数がオブジェクトなので、コンストラクタは Dog というラベルの付いた関数として実装され、そのクラスのプロトタイプに bark() メソッドを実装するといった具合。

で、prototype.js に含まれている Class-Style OO な機能を使うと、JavaScript でももう少しクラスベース風なシンタックスで書くことができるという塩梅です。例えば先の犬の例を prototype.js な OO にすると、

var Dog = Class.create();
Dog.prototype = {
    initialize : function (name) {
        this.name = name;
    },
    bark : function () {
        alert(this.name + ': わんわん');	
    }
}

dog = new Dog('しなもん');
dog.bark();

となります。prototype.js は Class というクラスを生成するクラス(紛らわしいな)提供するので、まず

var Dog = Class.create();

としてクラスを作って、あとは prototype に色々付けたしていくと。ちなみに

Dog.prototype = {
    initialize : function (name) {
        this.name = name;
    },
    bark : function () {
        alert(this.name + ': わんわん');	
    }
}

の部分のシンタックスはあまり見慣れないのでこれが prototype.js の機能なのかなと一瞬思っちゃうのですが、これは JavaScript 組み込みのリテラルで、

var obj = { x:1, y:2 };

と書くと x が 1、y が 2 なオブジェクトに obj という名前がつくというものです。つまり、

var obj = new Object;
obj.x = 1;
obj.y = 2;

と同じということ。Class を使うとインスタンス生成時に initialize メソッドが呼ばれるようになったり、prototype.js に含まれる Object.extend と併用することで

var Dog = Class.create();
Dog.prototype = (new Animal).extend({
    ....
})

というシンタックスで継承が実現できたりするようになります。

前置きが長くなりました。この prototype.js を使ってデザインパターンなコードを書いてみるというのが今回の趣旨です。題材はもちろん結城さんのデザパタ本。学習がてらいくつか実装してみたので暇をみて掲載していこうかなと。(本当は、実際のウェブアプリケーションを題材にデザインパターンを適用するコードとかの方が良さそうなんですが、いかんせん経験が乏しいものでどういう場合にクライアントサイド JavaScript でデザインパターンを使うことがあるかがまだまったく掴めてなかったり。)

まずは Iterator パターンです。

デザインパターンのコードはまずそのクラス群を利用するコードから見ていくのがわかりやすいです。Iterator パターンでは

var Main = Class.create();
Main.prototype = {
    initialize : function () {},
    main : function () {
        var shelf = new BookShelf();
        shelf.appendBook(new Book('実践ハイパフォーマンス MySQL'));
        shelf.appendBook(new Book('Perlクックブック'));
        shelf.appendBook(new Book('Blog Hacks'));
        
        var it = shelf.iterator();
        while (it.hasNext()) {
            var book = it.next();
            document.writeln(book.getName() + '<br>');
        }
    }
}

というコードがあって、

var client = new Main;
client.main();

と実行すると、

実践ハイパフォーマンス MySQL
Perlクックブック
Blog Hacks

と表示されるようなプログラムです。肝は

var it = shelf.iterator();
while (it.hasNext()) {
    var book = it.next();
    document.writeln(book.getName() + '<br>');
}

ですね。shelf という物の集合体があってそこからイテレータを取り出してそいつを回すとその物を辿っていける。物の集合体の実装がどんなデータ構造であれ、同じインタフェースで中のものをひとつずつ取り出していくことができる、と。

それで、デザパタ本のコードでは Java の interface をまず用意して、となるのですが例によって JavaScript は動的な言語かつ型がないのでサブクラスに実装を強制させるメカニズムがない。ということで、ここでは interface に相当するものは用意せずに Concrete クラスだけで実装します。

登場するクラスは、

  • Book
  • BookShelf
  • BookShelfIterator

の 3 つになります。

まずは集合体の中の個々の物である Book クラス。

var Book = Class.create();
Book.prototype = {
    initialize : function(name) {
        this.name = name;
    },
    getName : function() {
        return this.name;
    }
}

本のコードにならって getName() を用意してますが、JavaScript 的には book.name でアクセスしちゃってもいいかもですね。

次、本の入れ物であり集合体のクラスであるところの BookShelf。デザパタ本では Concrete Aggregate に相当するやつですね。

var BookShelf = Class.create();
BookShelf.prototype = {
    initialize : function() {
        this.last = 0;
        this.books = new Array();
    },
    getBookAt : function(index) {
        return this.books[index];
    },
    appendBook : function(book) {
        this.books[this.last] = book;
        this.last++;
    },
    getLength : function() {
        return this.last;
    },
    iterator : function() {
        return new BookShelfIterator(this);
    }
}

Java みたいにクラスの中では this を省略できるといいんだけど、それは無理っぽいのでこんな塩梅になりました。iterator メソッドでイテレーターを返します。

var BookShelfIterator = Class.create();
BookShelfIterator.prototype = {
    initialize : function(bookshelf) {
        this.bookshelf = bookshelf;
        this.index = 0;
    },
    hasNext : function () {
        return this.index < this.bookshelf.getLength();
    },
    next : function() {
        return this.bookshelf.getBookAt(this.index++);
    }
}

んでもってそのイテレータ。特に難しいところもなし。

僕はこのクラスのコードを、Main も含めて iterator.js という中に全部書いてやって

<script type="text/javascript" src="../prototype.js"></script>
<script type="text/javascript" src="iterator.js"></script>
<script type="text/javascript">
var main = new Main();
main.main();
</script>

と記述した HTML を用意し実行してます。

ちと長くなりましたがこんな感じで一つ一つのパターンのコードを見せプログラミングしていこうかなと。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

JavaScriptスキーJavaScriptスキー 2005/08/15 22:04 JavaScriptでデザインパターンを説明するとき、Java流のIteratorを取り上げるのはちょっと不適切だと思います。
なぜならJavaScriptは関数がオブジェクトなので、SmalltalkやRuby風の内部イテレータを使えるからです。
(内部イテレータについては http://capsctrl.que.jp/kdmsnr/wiki/bliki/?CollectionClosureMethod 参照)

var BookShelf = Class.create();
BookShelf.prototype = {
initialize : function() {
this.last = 0;
this.books = new Array();
},
getBookAt : function(index) {
return this.books[index];
},
appendBook : function(book) {
this.books[this.last] = book;
this.last++;
},
getLength : function() {
return this.last;
},
each : function(func) {
for (var i = 0; i < this.getLength(); i++) {
func(this.getBookAt(i));
}
}
}

var Main = Class.create();
Main.prototype = {
initialize : function () {},
main : function () {
var shelf = new BookShelf();
shelf.appendBook(new Book(’実践ハイパフォーマンス MySQL’));
shelf.appendBook(new Book(’Perlクックブック’));
shelf.appendBook(new Book(’Blog Hacks’));
shelf.each(function(book) {
document.writeln(book.getName() + ’
’);
});
}
}

   2006/01/02 02:45 内部イテレータが使えることはよく分かりましたが、外部イテレータであるJava流のイテレータがあっても不適切とは思えません。なぜならすべてにおいて内部イテレータが外部イテレータより勝っているとは考えにくいからです。この場合は併記でよいのでは。