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

7つめは、「複雑なインスタンスを組み立てる」Builder パターンです。

var Main = Class.create();
Main.prototype = {
    initialize : function () {},
    main : function () {
        var builder;
        if (arguments[0] == 'plain')
            builder = new TextBuilder();
        else
            builder = new HTMLBuilder();
        director = new Director(builder);
        director.construct();
        document.writeln(builder.getResult());
    }
}

というクライアントがあり、その出力は引数が plain なら

============================
『Greeting』

■朝から昼にかけて

  ・おはようございます。
  ・こんにちは。

■夜に

  ・こんばんは
  ・おやすみなさい
  ・さようなら

============================

という plain text な感じになり、そうじゃない場合は同じ内容が HTML になります。デザパタ本では HTML 出力はファイルになってアウトプットされてますが、ここではブラウザへの出力で実装します。

この出力は、まず文書のタイトルがある。残りは普通の文字列とか、リストがある。最後にフッタを出力。という構造になってます。なので、

  • 文書タイトルを作る
  • 普通の文字列を作る
  • リストを作る
  • フッタを出力する

という処理があって、これを plain text 版とか HTML 版を実装すると。その出力を組み立てるのは結構複雑な感じになりそうなので、Builder パターンを適用してみましょう、ってな話ですね。

plain text な Builder である TextBuilder、HTML な HTMLBuilder をそれぞれ実装していくわけですが、その抽象クラスをまず作ります。

Abstract.Builder = function() {};
Abstract.Builder.prototype = {
    makeTitle : function(title) {},
    makeString : function(str) {},
    makeItems : function(items) {},
    close : function() {},
    getResult : function() {}
}

て、これは実体のないメソッドのみなクラスなのでいわゆるインタフェースクラス。これまでの例では Java の interface に相当するものは省略してきましたが、Buiilder パターンはこれがないとパターンとして意味を成さないので、(サブクラスにメソッドのオーバーライドを強制させらんないので実質的には意味がないけども) 敢えて用意します。このクラスに用意されてるそれぞれのメソッドが、先の4つの処理に相当します。

で、この Builder の Concrete Builder クラス...の前に Builder を使う人を作ります。

var Director = Class.create();
Director.prototype = {
    initialize : function(builder) {
        this.builder = builder;
    },
    construct : function() {
        this.builder.makeTitle('Greeting');
        this.builder.makeString('朝から昼にかけて');
        this.builder.makeItems(new Array(
            'おはようございます。',
            'こんにちは。'
        ));
        this.builder.makeString('夜に');
        this.builder.makeItems(new Array(
            'こんばんは',
            'おやすみなさい',
            'さようなら'
        ));
        this.builder.close();
    }
}

コンストラクタで Concrete Builder のインスタンスを受け取って、そのインスタンスの各種メソッドを呼び出していきます。Concrete Builder の実装が何であれ、インタフェースは保証されてるので Director はそれを知らなくてもよい、ということです。

このインタフェースが保証されてる、ってとこに Abstract.Builder の存在意義があるわけで、Abstract.Builder がないとその辺がしっくりこなくなっちゃうんですよね。

Concrete Builder はそれぞれ以下の感じです。あ、文字列連結をなんとなく append メソッドでやりたかったので AppendableString なるクラスを作ってみました。(TextBuilder は plain text を出力するんですが、ブラウザで実行結果を確かめるために便宜上 pre タグを出力させてます。)

var TextBuilder = Class.create();
TextBuilder.prototype = (new Abstract.Builder).extend({
    initialize : function() {
        this.buffer = new AppendableString('');
    },
    makeTitle : function (title) {
        this.buffer.append('<pre>\n');
        this.buffer.append("============================\n");
        this.buffer.append("『" + title + "』\n");
        this.buffer.append('\n');
    },
    makeString : function (str) {
        this.buffer.append('■' + str + '\n');
        this.buffer.append('\n');
    },
    makeItems : function (items) {
        for (var i = 0; i < items.length; i++) {
            this.buffer.append("  ・" + items[i] + '\n');
        }
        this.buffer.append('\n');
    },
    close : function() {
        this.buffer.append("============================\n");
        this.buffer.append('</pre>\n');
    },
    getResult : function () {
        return this.buffer.toString();
    }
});

var HTMLBuilder = Class.create();
HTMLBuilder.prototype = (new Abstract.Builder).extend({
    initialize : function () {
        this.buffer = new AppendableString('');
    },
    makeTitle : function(title) {
        this.buffer.append('<html><head><title>' + title + '</title></head></body>');
        this.buffer.append('<h1>' + title + '</h1>');
    },
    makeString : function(str) {
        this.buffer.append('<p>' + str + '</p>');
    },
    makeItems : function(items) {
        this.buffer.append('<ul>');
        for (var i = 0; i < items.length; i++) {
            this.buffer.append('<li>' + items[i] + '</li>');
        }
        this.buffer.append('</ul>');
    },
    close : function() {
        this.buffer.append('</body></html>');
    },
    getResult : function() {
        return this.buffer.toString();
    },
});

var AppendableString = Class.create();
AppendableString.prototype = {
    initialize : function (str) {
        this.str = str;
    },
    append : function (str) {
        this.str = this.str + str;
    },
    toString : function () {
        return this.str;
    }
}

Builder パターンでは Director が Bulider をコントロールするので、メソッド呼び出しのフローは Director の中に実装されます。Template Method とよく似てますが、Template Method はフローを作るのはスーパークラスでやって、サブクラスで各メソッドの中身を実装する、というところが異なってます。

Director は Builder のメソッド呼び出しのフローを規定するわけですが、Director を使う側からみると、Director が「複雑なメソッド呼び出しをまとめて抽象化してシンプルな API (constructメソッド) を提供してくれてる」ということでこれは Facade パターンでもあります。

...って本の中で同じことが書いてた。