2007/09/27

Javascriptによる大規模開発の覚え書き

ポスト @ 2:33:03 , 修正 @ 2007/09/30 9:01:00 |     このエントリーを含むはてなブックマーク

未だに半年前のエントリにブクマされるみたいなので、もう少しjavascriptについて書いてみる。
今回は大規模化開発におけるJavascriptの注意点とかそういうの。当てはまらない環境の方もいます。(しかも基本的な事だらけで大したことは書いてないです)

ほぼリッチクライアントを主目的としたjavascripterとコードを対象とします。
どちらかというと、ライブラリを提供する側の視点から

1.ログを出力せよ

あなたが書いたコードは遅い、と必ず言われます。なので言われる前から、自分の書いたコードの処理時間をログするようにしましょう。

次のような処理時間を計測するロガーを作ります。

var TraceLog = function (){
    this.startTime = -1;
    var outer = document.getElementById('_outer');
    if(outer == null){
        outer = document.createElement('textarea');
        outer.id = '_outer';
        outer.value = '';
        document.body.appendChild(outer);
    }
    this.outer = outer;
};
TraceLog.prototype = {
    start: function(message){
        this.startTime = new Date().getTime();
        this.outer.value += '[' + message + '] has started \n';
    },
    stop: function(message){
        var current = new Date().getTime();
        var endTime = current - this.startTime;
        this.outer.value += '[' + message + '] has finished at ' + endTime + 'ms \n';
    }
};

後は、メソッドごとに記述するも良し、Profilerみたいのを作って、各メソッドとかに埋めるのも良いでしょう。

var Hoge = function (){};
Hoge.prototype = {
    methodA: function (){
        var trace = new TraceLog;
        trace.start('Hoge#methodA');
        alert('methodA');
        trace.stop('Hoge#methodA');
    }
};
var Foo = function (){};
Foo.prototype = {
    methodB: function (){
        alert('methodB');
    }
};

(function (){
    var traceInterceptor = function(target){
        for(var property in target){
            if(typeof target[property] == 'function'){
                var __method = target[property];
                target[property] = function (){
                    var log = new TraceLog;
                    log.start(property);
                    var returnValue = __method.apply(this, arguments);
                    log.stop(property);
                    return returnValue;
                };
            }
        }
    };
    for(var i = 0; i < arguments.length; i++){
        traceInterceptor(arguments[i].prototype);
    }
})(Hoge, Foo);

var hoge = new Hoge;
hoge.methodA();
var foo = new Foo;
foo.methodB();

ここで、問題となるのが、ログを出したくないのに出力されてしまう問題。
IEならば、JScriptのコンパイラオプション(条件付コンパイル?)を設定するのが楽です。

@set @trace = true

@if (@trace)
var TraceLog = function (){
    :
    :
};
TraceLog.prototype = {
    start: function(message){
        :
        :
    },
    stop: function(message){
        :
        :
    }
};
@else
var TraceLog = function (){
    this.start = this.stop = function (){/* nop */};
};
@end

それ以外のブラウザならば、@traceの値を何かしらの変数で定義し、通常のif分でコンパイルするのがいいでしょう。

ref - 条件付きコンパイル ステートメント

今回使用したtraceInterceptorは後記

2.要素の見た目を変更する処理は外部化せよ

よくありがちな、inputタグの背景色を変更するといったような処理は、面倒でもその部分は外部化できるようにしましょう。
なぜなら、見た目に反映される部分は修正する回数が多かったり、内部ロジックで閉じてしまったがため、変更するのが用意では無いことが多いため、必ず外部化できるようにします。

次のコードは、複数の箇所で色の定義を行ってしまっているため、あまりお薦めされません。

var FormLogicHoge = {
    validateA: function (input){
        // 文字数チェック
        if(input.value.length < 12){
            input.style.backgroundColor = '#f36';
        }
    },
    validateB: function (input){
        // 英数チェック
        if(/^[a-zA-Z1-9].+?/.test(input.value)){
            input.style.backgroundColor = '#3fc';
        }
    }
};
var FormLogicFoo = {
    doSomething: function(input){
        if(cond){
            input.style.backgroundColor = '#369';
        }
    }
};

上記の処理では、各ifで色を付ける処理が分散されてしまっている上、どの色が、どの状態にあたるのかが分かりづらいです。(コメントで判別するのは大変)
よって、次のように修正し、修正ポイントを明確にして修正が行いやすいようにします。

var FormLogic = {
    validateA: function (input){
        // 文字数チェック
        if(input.value.length < 12){
            Validator.overflow(input);
        }
    },
    validateB: function (input){
        // 英数チェック
        if(/^[a-zA-Z1-9].+?/.test(input.value)){
            Validator.numetic(input);
        }
    }
};

var Validator = {
    overflow: function (input){
        input.style.backgroundColor = '#f36';
    },
    numetic: function (input){
        input.style.backgroundColor = '#3fc';
    }
};

style属性を変更する場合は、stylesheetの!importantにも気を付けます。
また、style属性では個別に修正ポイントが増えてくるため、あらかじめCSSで外部化するがベストです。(欲をいうとvalidate処理も別メソッドで共通化しておきたいところ)

3.引数のJSON化(Hash化)せよ

主にprototype.jsが導入されているところでは違和感無くやっていることだと思いますが、引数にJSON(Hash ?)を渡すことで、 明示的にパラメータを定義しやすく混乱を招きにくいです。
また、javascriptは可変引数を受け付ける言語のため、引数が増えた場合の問題にも対処しやすいです。

以下はダメなサンプル

var createInput = function(id, type, defaultValue, tabIndex, readOnly){
    // default
    if(type == null){
        type = 'text';
    }
    if(defaultValue == ull){
        defaultValue = '';
    }
    if(tabIndex === null){
        tabIndex = 0;
    }
    if(readOnly === null){
        readOnly = false;
    }
    var input = document.createElement('input');
        input.id = id;
        input.type = type;
        input.value = defaultValue;
        input.tabIndex = tabIndex;
        input.readOnly = readOnly;
    return input;
};

var inputA = createInput('hogeId', 'text', '1', 2, false);
var inputB = createInput('fooId', 'hidden', '2', 1, true);
var inputC = createInput('barId', 'password');
var inputD = createInput('bazId', 'text', 'aaaa');

これでは、引数の順番で混乱してしまいがちです。また、引数が省略された場合に混乱してしまいそうです。
次のようにするのがいいかもしれません。(少し手抜きですが)

var createInput = function(id, options){
    var input = document.createElement('input');
        input.id = id;
        input.type = options.type || 'text';
        input.value = options.defaultValue || '';
        input.tabIndex = options.tabIndex || 0;
        input.readOnly = options.readOnly || false;
    return input;
};

var inputA = createInput('hogeId', {type: 'text', defaultValue: '1', tabIndex: 2, readOnly: false});
var inputB= createInput('fooId', {type: 'hidden', defaultValue: '2', tabIndex: 1, readOnly: true});
var inputC = createInput('barId', {type: 'password'});
var inputD = createInput('bazId', {defaultValue: 'aaaa'});

JSONコード(やprototype.jsのHash($H))を使えばもっと明示的に値を指定するのも楽になります。

4.例外を使用せよ

javascriptも例外を扱うことができる言語です。例外を上手く使用することで、バグの早期発見やバグを見付けた際に対処しやすい、UTテストがしやすいといった利点があります。

javascriptのデフォルトにErrorがあるので、これを使用するのが一番楽です。

var Hoge = {
    sliceString: function(str, from, to){
        if(str === null){
            throw new Error('引数strは必ず入力してください><');
        }
        if(typeof str != 'string'){
            throw new Error('引数strは文字列で入れて><');
        }
        if(from === null){
            throw new Error('引数fromは必ず入力してください><');
        }
        if(typeof from != 'number'){
            throw new Error('引数fromは数値で入れて><');
        }
        :
        : doSomething
        :
    }
};

try {
    Hoge.sliceString(null, 1, 2);
} catch(e) {
    alert(e.message);
}

もし、例外が増えてきて管理に困りそうになった場合は、例外時のハンドリングを行いたい場合は、例外クラスを複数作成するのがいいでしょう。

var RuntimeException = function (message){
    this.name = 'RuntimeException';
    this.message = message;
    this.description = this.name + ': message ' + this.message;
};
RuntimeException.prototype = new Error;

var NullPointerException = function(message){
    this.message = message;
};
NullPointerException.prototype = new RuntimeException;
NullPointerException.prototype.name = 'NullPointerException';

var IndexOutOfBoundsException = function(message){
    this.message = message;
};
IndexOutOfBoundsException.prototype = new RuntimeException;
IndexOutOfBoundsException.prototype.name = 'IndexOutOfBoundsException';

var Hoge = {
    doSomething: function(arg0, index){
        if(arg0 === null){
            throw new NullPointerException('arg0 was null');
        }
        if(arg0.length < index){
            throw new IndexOutOfBoundsException('arg0.length was ' + arg0.length);
        }
        if(cond){
            throw new RuntimeException('hoge');
        }
        :
        : doSomething
        :
    }
};

try {
    Hoge.doSomething('12345', 2);
} catch(e) {
    if(e instanceof NullPointerException){
        // doSomething
    }
    if(e instanceof IndexOutOfBoundsException){
        // doSomething
    }
    if(e instanceof RuntimeException){
        // doSomething
    }
}

また、try/catch/finallyもあるので、必ず戻り値を返すような処理では高度な例外処理も可能です。

この他にもevalしたときはSyntaxErrorを捕捉せよとかあるけど、まぁぼちぼち

5.高速化せよ

長くなるので、次回書く

[追記] こっちに書きました。
ref - ハタさんのブログ : Javascriptによる大規模開発の覚え書き。高速化編

6.interceptできるようにせよ

処理を隠蔽することは良いことです。予期しない処理を呼び出され、不安定になるよりも隠蔽し呼出を行えないように記述するのは正しい処置です。
しかしながら、先のTraceLogで使用したようにjavascriptではAOPのように処理を埋め込むことが可能なため、これを使用しない手はないです。

次に示すのは、とある要素に埋め込まれたonclick処理を取り消すこと無く、onclickイベントに処理を追加するサンプルです。

var inputProxy = function (id){
    var handler = function (input){
        console.log(input.value);
        return false;
    };
    var inputs = document.getElementById(id).getElementsByTagName('input');
    for(var i = 0, length = inputs.length; i < length; ++i){
        var input = inputs[i];
        var beforeonclick = input.onclick;
        input.onclick = function (){
            try {
                handler(this);
            } finally {
                beforeonclick.apply(this, arguments);
            }
        };
    }
};

これは、addEventListenerattachEventでも同じような事が可能ですが、処理を割り込んだりさせるには、こういった使いかたも可能です。(理由は他にもあるけど、いつか書く)
こうすることで、onclick時の前処理を共通化することが可能になり、処理を分散することを防げ、修正ポイントが明確になります。

onclickなどのように外部からでも参照可能な場合はまだいいですが、もしも、ローカルスコープにこういった処理が記述されてしまっている場合は、処理の埋め込みが困難になります。
上記のTraceLogをサンプルに、次のサンプルを示します。

var TraceLog = function (outer){
    this.startTime = -1;
    this.outer = outer;
    this.start = function(message){
        this.startTime = new Date().getTime();
        this.outer.value += '[' + message + '] has start \n';
    };
    this.stop = function(message){
        var current = new Date().getTime();
        var endTime = current - this.startTime;
        this.outer.value += '[' + message + '] has finished at ' + endTime + 'ms \n';
    }
};

(function (){
    var defaultOuter = document.getElementById('defaultOuter');
    var trace = new TraceLog(defaultOuter);
    // doSometingAの時はdefaultLogに出力
    trace.start('doSometingA');
    //
    // doSometingA
    //
    trace.stop('doSometingA');

    // outerの切替え
    trace.outer = document.getElementById('otherOuter');
    // doSometingBの時はotherOuterに出力
    trace.start('doSometingB');
    //
    // doSometingB
    //
    trace.stop('doSometingB');

    // 処理が終わったので、defaultOuterに戻す
    trace.outer = defaultOuter;
})();

もしこれが、ローカルスコープになってしまった場合の例を記述します。

var TraceLog = function (outer){
    this.startTime = -1;
    var outer = outer;
    this.start = function(message){
        this.startTime = new Date().getTime();
        outer.value += '[' + message + '] has start \n';
    };
    this.stop = function(message){
        var current = new Date().getTime();
        var endTime = current - this.startTime;
        outer.value += '[' + message + '] has finished at ' + endTime + 'ms \n';
    };
    this.changeOuter = function(o){
        outer = o;
    };
};

(function (){
    var defaultOuter = document.getElementById('defaultOuter');
    var trace = new TraceLog(defaultOuter);
    // doSometingAの時はdefaultLogに出力
    trace.start('doSometingA');
    //
    // doSometingA
    //
    trace.stop('doSometingA');

    // outerの切替え
    trace.changeOuter(document.getElementById('otherOuter'));
    // doSometingBの時はotherOuterに出力
    trace.start('doSometingB');
    //
    // doSometingB
    //
    trace.stop('doSometingB');

    // 処理が終わったので、defaultOuterに戻す
    trace.changeOuter(defaultOuter);
})();

ローカルスコープになった場合でも大きな変化はありませんが(サンプルが悪いんですが...)、outerをローカルスコープにしてしまったため、outerオブジェクトを変更するために、メソッドの追加が必要になりました。
もし、これが小規模の修正ならばいいのですが、範囲が大きい場合は大変な努力を必要とします。
ローカルスコープにすることでprivateにするのかは、賛否両論ありそうですが、使いどころを間違えると修正量が多くなったりするので注意がいります(varをthisにするだけで動くのであれば簡単なんですが、そうでない場合もありそうですし)

7.非同期処理はコールバックさせよ

理由は単純です、呼び出されたどうかがわからなくなるために、setTimeoutsetIntervalが行われた際にはコールバックを仕込んでおきます。

以下はそのテンプレート

Function.prototype.interval = function(long, callback){
    var __callback = callback || function (){};
    var id = setInterval(function (){
        __method.apply(this, arguments);
        __callback.apply(this, arguments);
        clearInterval(id);
    }, long);
};
Function.prototype.timeout = function(long, callback){
    var __method = this;
    var __callback = callback || function (){};
    var id = setTimeout(function (){
        __method.apply(this, arguments);
        __callback.apply(this, arguments);
        clearTimeout(id);
    }, long);
};

var hoge = function(){
    alert("hoge");
};

(function(){
    alert("timeout 100");
}).timeout(100, hoge);

まとめ

長文になってしまった・・・反省。

まだまだ書くこと、まとめることは沢山あるので、近い内に書きます。

ってことで、つづく。


3 Trackbacks

[Django][Python][senna][その他]巡回

Google Code: New: deseb django-blogging-system Update: djulog fluidrecords serpantin komercha django-categories Blog: オープンソースのEコマースソフトいろいろ [django] POST の判定方法 Tools for optimizing your website: Etag and Expire headers in Django, A

From : 常山日記 @ 2007-09-27 17:58:27

Hash化したらチェック忘れずに、てことでチェック用関数

@import "/twk/nondrupal/dp.SyntaxHighlighter/Styles/SyntaxHighlighter.css"; Javascriptによる大規模開発の覚え書き がずいぶんブックマークされているようですが、「3.引数のJSON化(Hash化)せよ」については、

From : ふらっと @ 2007-09-30 01:23:34

ちょっとした規模でのJavaScript覚え書き(オブジェクトの生成

こんにちは高橋です。JavaScriptを触る機会がそれなりにあったので、少しばかり開発の際の覚え書きを少しばかり書いておきます。 JavaScriptをちょっとした規模で利用する場合には、オブジ

From : 株式会社システムフレンド @ 2008-07-22 08:59:18

Track from Your Website

http://blog.xole.net/trackback/tb.php?id=612

Comment

No Comments

Post Your Comment


*は入力必須です。E-Mailは公開されません。

1 + 2 =