Chapter 5. Object Creation Patterns
2011/01/14(金) 19:29 Javascript親記事へこのエントリーをはてなブックマークに追加

Namespace Pattern

グローバルに一つの名前空間をおいてそこを拡張していく方法
// global object
var MYAPP = {};
// constructors
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// a variable
MYAPP.some_var = 1;
// an object container
MYAPP.modules = {};
// nested objects
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};
この方法はすべにプレフィックスを置く必要があるので総コード量が増える。
またネストが深くなると名前解決のlookupも遅くなる。

General Purpose Namespace Function

上の書き方はすでに同じ名前のグローバルオブジェクトが存在していたことを考慮していなかったので、次のように書きます。
// unsafe
var MYAPP = {};
// よりよい方法
if (typeof MYAPP === "undefined") {
    var MYAPP = {};
}
// 短く書くと
var MYAPP = MYAPP || {};
プロパティに追加する場合も同様で、追加したいオブジェクトが存在しているかを確認してからでないと、存在しないオブジェクトに対してプロパティを追加しようとすればエラーになります。
たとえば MYAPP.modules.module2 を持つようなオブジェクトを作ろうとすると、以下のように順番立ててオブジェクトを作らないといけないので面倒です。
// equivalent to:
var MYAPP = {
     modules: {
         module2: {}
     }
};
これ次のようにシンプルに書ける関数を作ってみましょう。
MYAPP.namespace('MYAPP.modules.module2');
MYAPPの名前空間を拡張する関数
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
    var parts = ns_string.split('.'),
            parent = MYAPP,
            i;
    // strip redundant leading global
    if (parts[0] === "MYAPP") {
        parts = parts.slice(1);
    }
    for (i = 0; i < parts.length; i += 1) {
        // create a property if it doesn't exist
        if (typeof parent[parts[i]] === "undefined") {
            parent[parts[i]] = {};
        }
        parent = parent[parts[i]];
    }
    return parent;
};
この関数を使えば下のように簡単にネストの深い名前空間を作成できます。
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true

Declaring Dependencies

YUI2の場合、次のようにYAHOO.util.Eventといったようにモジュールに分かれている。
ローカルで使う際にエイリアスを張るシンプルなパターン
var myFunction = function () {
    // dependencies
    var event = YAHOO.util.Event,
        dom = YAHOO.util.Dom;
    // use event and dom variables
    // for the rest of the function...
};
これのいい点としてはローカルで何を使うのか明示的に宣言できる点や
eventといったローカル変数はYAHOOと言ったグローバル変数を参照するより高速になること。
またYUICompressor や Google Closure compilerといった圧縮ツールはeventをaなどの短い文字列に変えてくれる(グローバル変数の方が危険なので変更されない)のでローカルでこのモジュールを使うほど圧縮した際にコードの量はエイリアスを張らない時に比べて差が出る。

Private Properties and Methods

JavaScriptにはprivateにするといった演算子はないのでどうする?

Private Members

クロージャーを使用することでprivate menmverを作成できる。
function Gadget() {
    // private member
    var name = 'iPod';
    // public function
    this.getName = function () {
        return name;
    };
}
var toy = new Gadget();
// 'name' is undefined,
console.log(toy.name); // undefined

Privileged Methods

一個前の getName()は特権的なメソッドで、nameプロパティへのアクセスすることができる。
(クロージャーの話ですね

Privacy Failures

基本的にはnameプロパティには外部からアクセスできないけど、いくつかの例外的なケースが存在する
  • eval() の第二引数 , __parent__ を使った方法(もう廃止されてる気もする)
  • 特権的なメソッドがオブジェクトや配列と形で値を返す時、それらを外から書き換えることでprivateな値にアクセスできる
実際に例として見てみるとgetSpecs()はオブジェクトを返している。
その返されたオブジェクトを書き換えることでprivateなものも書き換えできてしまっている。
function Gadget() {
    // private member
    var specs = {
        screen_width:  320,
        screen_height: 480,
        color: "white"
    };
    // public function
    this.getSpecs = function () {
        return specs;
    };
}
var toy = new Gadget(),
specs = toy.getSpecs();
console.dir(specs); // 返り値のオブジェクト
specs.color = "black";// 書き換え
specs.price = "free";

var toy_2 = new Gadget(),
specs_2 = toy.getSpecs();
console.dir(specs_2);// オブジェクトを書き換えられてしまった
対処法としては
  • Principle Of Least Authority (POLA) は「最小権限の原則」で返すものを最小限にする
  • deep copyを作ってそれを返す

Object Literals and Privacy

オブジェクトリテラルの場合を見てみる
いわゆるモジュールパターンと呼ばれるもの。
var myobj = (function () {
    // private members
    var name = "my, oh my";
    // implement the public part
    return {
        getName: function () {
            return name;
        }
    };
}());
myobj.getName(); // "my, oh my"

Prototypes and Privacy

上記のようにコンストラクタでthisを使ってやる方法には、毎回オブジェクトに複製されるためメモリをもりもり食べるという問題があります。
そういう時にはprototypeプロパティを使ってインスタンスオブジェクトで共有させるのがいいでしょう。
function Gadget() {
    // private member
    var name = 'iPod';
    // public function
    this.getName = function () {
        return name;
    };
}
// prototype - インスタンスで共有
Gadget.prototype = (function () {
    // private member
    var browser = "Mobile Webkit";
    // public prototype members
    return {
        getBrowser: function () {
            return browser;
        }
    };
}());
var toy = new Gadget();
console.log(toy.getName()); // privileged "own" method
console.log(toy.getBrowser()); // privileged prototype method

Revealing Private Functions As Public Methods

ECMAScript 5にはオブジェクトを変更できなくさせるオプションあるよ。
でも今のバージョンにはないのでrevelation patternを使う。(“revealing module pattern”)
var myarray;
(function () {
    var astr = "[object Array]",
        toString = Object.prototype.toString;
    function isArray(a) {
        return toString.call(a) === astr;
    }
    function indexOf(haystack, needle) {
        var i = 0,
            max = haystack.length;
        for (; i < max; i += 1) {
            if (haystack[i] === needle) {
                return i;
            }
        }
        return −1;
    }
    // ここ
    myarray = {
        isArray: isArray,
        indexOf: indexOf,
        inArray: indexOf
    };
}());
myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2
// 仮にmyarray.indexOfが書き換えられても、privateなとこまでは変えられない。
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2
(言ってる意味がよくわからなかった

Module Pattern

モジュールパターンはいくつかのパターンとして組み合わせて利用できたりするのでよく使われている。
何かのオブジェクトを拡張したりするときに使える。
MYAPP.utilities.array = (function () {
    // この辺にprivateなmenber
    var ...
    // public API
    return {
        inArray: function (needle, haystack) {
            // ...
        },
        isArray: function (a) {
            // ...
        }
    };
}());

Revealing Module Pattern

さっきの revelation patternをモジュールに適応してみる。
基本的にはあんまり変わらないように見えるけど、こっちの方がデバッガーには優しい。
MYAPP.utilities.array = (function () {
        // private properties
    var array_string = "[object Array]",
        ops = Object.prototype.toString,
        // private methods
        inArray = function (haystack, needle) {
            for (var i = 0, max = haystack.length; i < max; i += 1) {
                if (haystack[i] === needle) {
                    return i;
                }
            }
            return −1;
        },
        isArray = function (a) {
            return ops.call(a) === array_string;
        };
        // end var
    // revealing public API
    return {
        isArray: isArray,
        indexOf: inArray
    };
}());

Modules That Create Constructors

さっきのと大きな違いは返すのはオブジェクトではなく関数であること。
MYAPP.namespace('MYAPP.utilities.Array');
MYAPP.utilities.Array = (function () {
        // dependencies
    var uobj  = MYAPP.utilities.object,
        ulang = MYAPP.utilities.lang,
        // private properties and methods...
        Constr;
        // end var
    // optionally one-time init procedures
    // ...
    // public API -- constructor
    Constr = function (o) {
        this.elements = this.toArray(o);
    };
    // public API -- prototype
    Constr.prototype = {
        constructor: MYAPP.utilities.Array,
        version: "2.0",
        toArray: function (obj) {
            for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
                a[i] = obj[i];
            }
            return a;
        }
    };
    // return the constructor
    // to be assigned to the new namespace
    return Constr;
}());
// 使う時は
var arr = new MYAPP.utilities.Array(obj);

Importing Globals into a Module

モジュールパターンはエイリアスがはれるので便利。
MYAPP.utilities.module = (function (app, global) {
// app, global でアクセスできる
}(MYAPP, this));

Sandbox Pattern

Sandbox Patternはnamespacing patternの欠点に基づいてる。
namespacing patternの欠点をあげてみると以下のような問題がある。
  • 一つのグローバルオブジェクに依存するため、同じページで2つのバージョンのライブラリやアプリを動かせない。
  • モジュールで分けていくとMYAPP.utilities.arrayのようにドットが深くなる(名前解決のコスト)
YUI 3ではこのパターンが使われている。

A Global Constructor

namespacing patternは一つのグローバルオブジェクトだが、Sandbox Patternは一つのSandbox()というコンストラクタを使う。
// 必要なモジュールを決めて使える
Sandbox(['ajax', 'event'], function (box) {
    // console.log(box);
});
// 配列にしなくてもいい
Sandbox('ajax', 'dom', function (box) {
    // console.log(box);
});
// すべてのモジュールが必要な時は*を使うか省略する
Sandbox('*', function (box) {
    // console.log(box);
});
Sandbox(function (box) {
    // console.log(box);
});
//Sandboxは入れ子にしても利用できる。
Sandbox('dom', 'event', function (box) {
    // dom and eventを使う処理
    Sandbox('ajax', function (box) {
    // ajaxを使う処理
});

Adding Modules

Sandboxコンストラクタを実装する前にモジュールの追加方法を見てみる。
boxをインスタンスとして追加していく。
Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
    box.getElement = function () {};
    box.getStyle = function () {};
    box.foo = "bar";
};
Sandbox.modules.event = function (box) {
    // Sandbox prototypeへのアクセスが必要ならcostructorをたどる
    // box.constructor.prototype.m = "mmm";
    box.attachEvent = function () {};
    box.dettachEvent = function () {};
};
Sandbox.modules.ajax = function (box) {
    box.makeRequest = function () {};
    box.getResponse = function () {};
};

Implementing the Constructor

最後にSandbox()コンストラクタを実装してみよう。
function Sandbox() {
    // 引数は配列argsに変換
    var args = Array.prototype.slice.call(arguments),
        // 配列の最後はコールバック関数なので取り出す
            callback = args.pop(),
        // Sandbox(["A","B"],callback) or Sandbox("A","B",callback) どちらでも可能
            modules = (args[0] && typeof args[0] === "string") ? args : args[0],
            i;
    // newを付けないで呼ばれたときにコンストラクタとして呼び直す
    if (!(this instanceof Sandbox)) {
        return new Sandbox(modules, callback);
    }
    // 必要ならthisにプロパティを追加できるよ
    this.a = 1;
    this.b = 2;
    // now add modules to the core `this` object
    // moduleがない=パラメータレス or *を指定したときは全てのモジュールを使う
    if (!modules || modules === '*') {
        modules = [];
        for (i in Sandbox.modules) {
            if (Sandbox.modules.hasOwnProperty(i)) {
                modules.push(i);
            }
        }
    }
    // 必要としてモジュールの初期化
    for (i = 0; i < modules.length; i += 1) {
        Sandbox.modules[modules[i]](this);
    }
    // Sandboxのコールバック(処理本体)を呼ぶ
    callback(this);
}
// prototypeプロパティとして定義すれば全部のモジュールから使える
// モジュール内からならbox.constructor.prototypeって書いたやつと同じ
Sandbox.prototype = {
    name: "My Application",
    version: "1.0",
    getName: function () {
        return this.name;
    }
};

Static Members

Public Static Members

この動きおもしろい。
// constructor
var Gadget = (function () {
    // static variable/property
    var counter = 0,
        NewGadget;
    // this will become the
    // new constructor implementation
    NewGadget = function () {
        counter += 1;
    };
    // a privileged method
    NewGadget.prototype.getLastId = function () {
        return counter;
    };
    // overwrite the constructor
    return NewGadget;
}()); // execute immediately
// test
var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId();   // 2
var ipad = new Gadget();
ipad.getLastId();   // 3

Object Constants

JavaScriptにはconstがないので、定数を作っても書き換える事ができてしまいます。
次善策としてはNumber.MAX_VALUE;などのビルトインに従って大文字_のルールで変数を作るなどがあります。
もう一つは書き換えができないようにconstantオブジェクト経由で定数を操作する方法
var constant = (function () {
    var constants = {},
        ownProp = Object.prototype.hasOwnProperty,
        allowed = {
            number: 1,
            boolean: 1
        },
        prefix = (Math.random() + "_").slice(2);
    return {
        set: function (name, value) {
            if (this.isDefined(name)) {
                return false;
            }
            if (!ownProp.call(allowed, typeof value)) {
                return false;
            }
            constants[prefix + name] = value;
            return true;
        },
        isDefined: function (name) {
            return ownProp.call(constants, prefix + name);
        },
        get: function (name) {
            if (this.isDefined(name)) {
                return constants[prefix + name];
            }
            return null;
        }
    };
}());
// TEST
// まだ未定義
constant.isDefined("maxwidth"); // false
// 定義する
constant.set("maxwidth", 480); // true
// check again
constant.isDefined("maxwidth"); // true
// 上書きはできない
constant.set("maxwidth", 320); // false
// is the value still intact?
constant.get("maxwidth"); // 480

Chaining Pattern

メソッドチェーンの作り方
myobj.method1("hello").method2().method3("world").method4();
このパターンはthisを返す事で作成できます。
var obj = {
    value: 1,
    increment: function () {
        this.value += 1;
        return this;
    },
    add: function (v) {
        this.value += v;
        return this;
    },
    shout: function () {
        alert(this.value);
    }
};

Pros and Cons of the Chaining Pattern

メソッドチェーンを書きすぎてデバッグがしにくくなることを"列車事故"という。via Clean Code - Robert Martin
まあ便利なのでjQueryとかDOM APIでも使われている。

method() Method

methodというprototype拡張をするメソッド(オリジナルは Douglas Crockford )
if (typeof Function.prototype.method !== "function") {
    Function.prototype.method = function (name, implementation) {
        this.prototype[name] = implementation;
        return this;// メソッドチェーン用
    };
}
これを使うと以下のようにコンストラクタが書ける。
var Person = function (name) {
    this.name = name;
}.
    method('getName', function () {
        return this.name;
    }).
    method('setName', function (name) {
        this.name = name;
        return this;
    });
// TEST
var a = new Person('Adam');
a.getName(); // 'Adam'
a.setName('Eve').getName(); // 'Eve'

おわり

Sandbox()はおもしろい感じ

名前:  非公開コメント   

  • TB-URL  http://efcl.info/adiary/093/tb/