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

ようやく8つめ。まだ残り 15 あります。8つめは「関連する部品を組み合わせて製品を作る」Abstract Factory パターン。複数のオブジェクトを正しい組み合わせで生成されるようにしたい、といったときに使うパターンです。

デザパタ本の題材は階層構造を持ったリンク集ページを作る、というものですね。

LinkPage

    * 新聞
          o 朝日新聞
          o 読売新聞
    * サーチエンジン
          o Yahoo!
                + Yahoo!
                + Yahoo! Japan
          o Excite
          o Google

-- 
伊藤 直也

こんな感じの出力を HTML で出してみましょう、というもの。

集合を扱う Tray クラスと、各個別のリンクを扱う Link クラスを用意してそのインスタンスを操作しページに相当する Page クラスに追加していくことでこの出力を作ります。ただ、出力は ul/li によるリスト構造による出力とか table による出力とか色々なパターンを用意したい。

そこで、出力の種類に応じて ul/li のリストのときは

  • ListTray
  • ListLink
  • ListPage

という List* な三つのクラスを組み合わせて使う。一方、table の場合は

  • TableTray
  • TableLink
  • TablePage

という三つのクラスを組み合わせて使うと。

出力に応じてクラスの組み合わせが異なってくるので、ちゃんと正しい組み合わせでインスタンスを生成できるように Abstract Factory パターンを使いましょうということになります。リストの場合は ListFacotry、table の場合は TableFactory。これらを切り替えることでインスタンスの組み合わせをごっそり入れ替えることもできます。

クライアントは以下になります。

var Main = Class.create();
Main.prototype = {
    initialize : function () {},
    main : function(classname) {
        var factory  = Factory.getFactory(classname);
        var asahi    = factory.createLink('朝日新聞', 'http://www.asahi.com/');
        var yomiuri  = factory.createLink('読売新聞', 'http://www.yomiuri.co.jp/');
        var us_yahoo = factory.createLink('Yahoo!', 'http://www.yahoo.com/');
        var jp_yahoo = factory.createLink('Yahoo! Japan', 'http://www.yahoo.co.jp/');
        var excite   = factory.createLink('Excite', 'http://www.excite.com/');
        var google   = factory.createLink('Google', 'http://www.google.com/');

        var traynews = factory.createTray('新聞');
        traynews.add(asahi);
        traynews.add(yomiuri);

        var trayyahoo = factory.createTray('Yahoo!');
        trayyahoo.add(us_yahoo);
        trayyahoo.add(jp_yahoo);

        var traysearch = factory.createTray('サーチエンジン');
        traysearch.add(trayyahoo);
        traysearch.add(excite);
        traysearch.add(google);

        var page = factory.createPage('LinkPage', '伊藤 直也');
        page.add(traynews);
        page.add(traysearch);
        page.output();
    }
}

それぞれの部品はすべて Facotry によって作成されます。どの Factory を選んだかによって生成されるインスタンスは異なるのですが、インタフェースが同一であると保証されてるので、いま何のオブジェクトをいじってるのか意識する必要がありません。

また、

var factory  = Factory.getFactory(classname);

として引数にクラス名を与えて、そのクラス名の Factory を生成してます。どの Factory (ListFactory なのか、TableFactory なのか) を使うのかも抽象化しているというのがポイントですね。

余談になりますが、はてなフレームワークURI と、ロジックが書かれたクラスが一致するような構成になっていて、URI に合わせてその URI に関連する一連のクラスを自動でインスタンス化して実行、なんてことをやってます。(なので、URIに対応したクラスの追加だけで新しいページが作れる。) そこで Abstract Facotry を使っています。

前置きが長くなりました。

クライアントでは入れ物の Tray と中身の Link を同じインタフェースでアクセスできるようにしてるようです。これは Composite パターンですね。

と、いうことでまずはその Tray と Link を同一視するためのクラスであるところの Item を作ります。

Abstract.Item = function () {};
Abstract.Item.prototype = {
    initialize : function (caption) {
        this.caption = caption;
    },
    makeHTML : function () {}
}

次にこれを継承した抽象クラス(抽象クラスをベースにした抽象クラス)である Link と Tray を作ります。この Link と Tray を継承して ListLink とか ListTray とかができあがるっていう段取りです。

Abstract.Link = function () {};
Abstract.Link.prototype = (new Abstract.Item).extend({
    initialize : function(caption, url) {
        Abstract.Item.prototype.initialize.apply(this, arguments);
        this.url = url;
    }
});

Abstract.Tray = function () {};
Abstract.Tray.prototype = (new Abstract.Item).extend({
    initialize : function(caption) {
        Abstract.Item.prototype.initialize.apply(this, arguments);
        this.tray = new Array;
    },
    add : function (item) {
        this.tray.push(item);
    }
});

initialize では Java のsuper() に相当する、initialize.apply(this, arguments) を呼び出してます。Tray のデータ構造は本では ArrayList ですが、ここでは例によって Array で代用。

次に、出力するページに相当する Page クラスを作ります。これも抽象クラスですね。

Abstract.Page = function () {};
Abstract.Page.prototype = {
    initialize : function (title, author) {
        this.title = title;
        this.author = author;
        this.content = new Array;
    },
    add : function (item) {
        this.content.push(item);
    },
    output : function() {
        document.writeln(this.makeHTML());
    },
    makeHTML : function() {}
}

makeHTML() が抽象メソッドです。本では output() の中はファイル生成処理ですが、ここではブラウザに直接出力するよう実装してます。

そして、部品を生成する Facotry。これも抽象クラスです。Abstract Factory パターンの中核になるクラスですね。

Factory = function () {};
Factory.prototype = {
    createLink : function (caption, url) {},
    createTray : function (caption) {},
    createPage : function (title, author) {},
}
Factory.getFactory = function(classname) {
    return eval('new ' + classname);
}

いままでの例だと抽象クラスは Abstract.Foo という名前で作ってましたが、このクラスはクライアントで直接使うので、なんとなく Abstract と prefix が付いてるのが気持ち悪かったのでこの名前で。

注目すべきはクラスメソッドの getFacotry() です。引数で与えられたクラス名を元に動的にクラス名を決定してインスタンスを生成するというロジック。Java では

Class.forName(classname).newInstance()

とかしますが、JavaScript では

eval('new ' + classname);

と単に eval で実行してやればいいだけです。

さて、これでインタフェースを規定する抽象クラス群はできあがりました。あとは Concrete クラス群を作っていけば完了。今回は List* だけ作りました。

  • ListLink
  • ListTray
  • ListPage
  • ListFacotry

を作ります。

ListLink = Class.create();
ListLink.prototype = (new Abstract.Link).extend({
    makeHTML : function() {
        return '<li><a href="' + this.url + '">' + this.caption + '</a></li>\n';
    }
});

ListTray = Class.create();
ListTray.prototype = (new Abstract.Tray).extend({
    makeHTML : function() {
        var buffer = new AppendableString('');
        buffer.append('<li>\n');
        buffer.append(this.caption + '\n');
        buffer.append('<ul>\n');
        for (var i = 0; i < this.tray.length; i++) {
            buffer.append(this.tray[i].makeHTML());
        }
        buffer.append('</ul>\n');
        buffer.append('</li>\n');
        return buffer.toString();
    }
});

var ListPage = Class.create();
ListPage.prototype = (new Abstract.Page).extend({
    makeHTML : function() {
        var buffer = new AppendableString('');
        buffer.append('<h1>' + this.title + '</h1>\n');
        buffer.append('<ul>\n');
        for (var i = 0; i < this.content.length; i++) {
            buffer.append(this.content[i].makeHTML());
        }
        buffer.append('</ul>\n');
        buffer.append('<hr><address>' + this.author + '</address>');
        return buffer.toString();
    }
});

var ListFactory = Class.create();
ListFactory.prototype = (new Factory).extend({
    initialize : function() {},
    createLink : function(caption, url) {
        return new ListLink (caption, url);
    },
    createTray : function(caption) {
        return new ListTray(caption);
    },
    createPage : function(title, author) {
        return new ListPage(title, author);
    },
});

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;
    }
}

これで完成。Table* を作るときも同様の手順だし、plain text な Text* を追加するとか PDF* を追加するとかいろいろ考えられますね。このようにアプリケーションに新しい出力種類を追加するのに Concrete クラス群を追加するだけでよい、その仕掛けは Factory がインスタンスの組み合わせを知っており、且つ eval で動的に Facotry を切り替えられる所、というのがミソです。