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

結城さんのデザパタMLでも紹介されてしまった手前、さぼるわけにもいくまい、ということで「機能の階層と実装の階層を分ける」 Bridge パターンです。

使いどころはたくさんありそうでなさそうでありそうな(どっちだ)、個人的には結構好きなパターンです。プログラムを拡張するには、クラスを追加するわけですが、あらかじめ「機能」を追加するのか、「実装」を追加するのかという視点で二つの階層に分けて実装しておくことで、拡張しやすい構成にしましょうというパターンです。

+-------------+
|Hello, Japan.|
+-------------+
+-------------+
|Hello, World.|
+-------------+
+----------------+
|Hello, Universe.|
+----------------+

こんな感じの出力を作りたい場合、まず考えられるのは

  • "Hello, Japan" とか "Hello, Universe" とか文字列を与える
  • その文字列を装飾して表示する

というクラスが考えられます。で、

+----------------+
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
|Hello, Universe.|
+----------------+

という出力も作りたい場合には

  • 文字列を繰り返し、それを装飾して表示する

という「機能」を拡張しましょうということになります。これは元のクラスが Template Method にでもなっていれば、それを継承してメソッドを追加することで拡張できそうです。

一方で、その文字列の与え方ですが

  • プログラムの中の String クラスから取得する
  • ファイル名から読み取る
  • データベースから読み取る

なんていう方法が考えられます。こっちが「実装」。この「機能」と「実装」をごちゃまぜにせずに綺麗に分離して、その両者を橋渡ししましょうっていうのが Bridge パターン。って、なんか気付けばパターンの解説をしている、いかんいかん。趣旨はあくまで prototype.js でパターンを実装するというものです。

var Main = Class.create();
Main.prototype = {
    initialize : function () {},
    main : function() {
        var d1 = new Display (new StringDisplayImpl("Hello, Japan."));
        var d2 = new CountDisplay(new StringDisplayImpl("Hello, World."));
        var d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe."));

        document.writeln('<pre>');
        d1.display();
        d2.display();
        d3.display();
        d3.multiDisplay(5);
        document.writeln('</pre>');
    }
}

こんな感じのクライアントがあって、ここから使うクラスをそれぞれ実装していきましょう。どこから見ていくのがいいかなと迷いましたが、書籍の例では実装の階層が低レベルのAPI、機能の階層が高レベルAPIという位置づけになってるっぽいので、実装の階層から見ていくほうが分かりやすそう、ということでそちらから。

Abstract.DisplayImpl = function () {};
Abstract.DisplayImpl.prototype = {
    rawOpen  : function () {},
    rawPrint : function() {},
    rawClose : function() {},
}

これは Java でいうところの interface。何度も言う用に、JavaScript では実質意味のないクラスです。が、Bridge パターンを説明するのにインタフェースがないと、実装の階層というのがなんだかよくわからないので、便宜上作ります。このインタフェースが実装側の API を規定している、ということになります。

var StringDisplayImpl = Class.create();
StringDisplayImpl.prototype = (new Abstract.DisplayImpl).extend({
    initialize : function(string) {
        this.string = string;
    },
    rawOpen : function() {
        this.printLine();
    },
    rawPrint : function() {
        document.writeln('|' + this.string + '|');
    },
    rawClose : function() {
        this.printLine();
    },
    printLine : function() {
        document.write('+');
        for (var i = 0; i < this.string.length; i++) {
            document.write('-');
        }
        document.writeln('+');
    }
});

その API を実装したクラスです。これは string が与えられてそれを表示するクラス。ファイルから読み取ってほげほげだったらコンストラクタでファイル名なりファイル関連のクラスを受け取って、rawOpen で開いて rawPrint で出力して rawClose でクローズ、というクラスを追加すると良い、と。これが「実装の追加」ですね。

一方、この実装を使う「機能」の階層です。ちょっと書籍とメソッドの実装順番を変えてます。

var Display = Class.create();
Display.prototype = {
    initialize : function(impl) {
        this.impl = impl;
    },
    display : function() {
        this.open();
        this.print();
        this.close();
    },
    open : function() {
        this.impl.rawOpen();
    },
    print : function() {
        this.impl.rawPrint();
    },
    close : function() {
        this.impl.rawClose();
    },
}

var CountDisplay = Class.create();
CountDisplay.prototype = (new Display).extend({
    multiDisplay : function(times) {
        this.open();
        for (var i = 0; i < times; i++) {
            this.print();
        }
        this.close();
    }
});

Display クラスがシンプルに一行で表示する display() を提供するクラス、CountDisplay() が指定した回数繰り返して表示する multiDisplay() を提供するクラス。クライアント(Main)から見た場合の API となるのがこの display() と multiDisplay() で、それぞれ Template Method パターンになってる模様。

さらに、実装側の rawOpen(), rawPrint(), rawClose() を直接使うことはせずに、open(), print(), close() という三つのメソッドで一段階抽象化のレイヤを挟んでから使っていると。これは Adapter パターン。

機能を追加したかったら Display なり CountDisplay なりを継承して open(), print(), close() という、機能の階層で与えられた API を使ったメソッドを追加しましょうとなります。

JavaScript 的に特筆するところは特にないかな。

Bridge パターンはプロパティとかメソッドのアクセスコントロールのメカニズムをうまく使って、実装の層で機能を拡張してしまったりするのを防ぐとか、実装の層の API と機能の層の API をごちゃまぜにして使うということをできないようにするとかいうことをしないとすぐ崩れてしまうような気もする。ので、Perl とか JavaScript みたいな、言語組み込みでそのメカニズムがない場合はちょっと工夫してやらないといけないかなあ、とも思います。