2007/09/30

Javascriptによる大規模開発の覚え書き。高速化編

ポスト @ 8:32:45 , 修正 @ 2007/10/01 22:10:47 |     このエントリーを含むはてなブックマーク

前回書いた「Javascriptによる大規模開発の覚え書き」が凄いことになってました。
今回は、省略した「5.高速化せよ」について書きます。

僕にとってjavascriptは非常に高速な言語です。それは何が高速か

開発速度が高速である
開発速度、及びそこに至るまでの修得速度はとても高速です。動的言語を上手く操る開発者はもちろん、開発に不慣れな(言葉が悪いけど)新人達でさえ「動く」モノをサクっと作ってしまえる。
また、プラットフォーム(? というかブラウザ)が広く普及しているので、ググればスグに問題解決もできる。
それにローカルで簡単に作れる。javascript、それは動作環境を含めて高速です。
高速にUI操作ができる(UI操作が非常に簡単である)
swingとかでUI操作をするには多くのオブジェクト操作をしなければならないけど、javascript(もといDHTML)は非常に簡単にUI(style)操作ができる。
これは、開発速度にも係わってくるけど、見た目が変更されやすい(修正量が多い)UIだけに、この高速さはヤバい。
CSSとHTML、それらをちゃんと抑えておけば修正量を少なく、高速な開発ができます。
修正に対するアクションが高速である
動的言語という強みは、やはり修正に対する量が如何に少ないかではないでしょうか。javascriptという動的言語の特性を活かせば、既に定義済みのメソッドに対する再定義やinterceptorが非常に簡単に行える。
javascriptは修正にかかる時間が非常に少なく。高速です。

ここまで早い言語を遅くする必要はありません。高速な言語は高速に。
以下それに対応する覚え書き

1. 処理の高速化

javascriptの処理速度なんかはやっぱちょっと遅いです。だからどこが遅いのか、どうやったら早くなるのかは抑えておくべきです。
「javascript 高速化」でググればいくつもの文章がみつかりますので、ここで僕が書くことは、あんまりありません。
「作っているときから高速化しましょう」

ちなみに、開発を開始する際には以下のプレゼン資料を参考にしました
ref - IT戦記 - Shibuya.js Technical Talk #1 を終えて。

2. 反映の高速化

いわゆる設定値の一元化をします。
良く使うcssのclassNameとかはもちろん、共用される値、定数値や判定につかう値は一元化しておきます。
また、設定値であるため、<script src="...">で読み込む際には必ず最初にくるようにします。

var Configure = {
    className: {
        /** validate-error時のclassName */
        error: 'validate-error',
        /** 強調文字列のclassName */
        important: 'strong',
        /* olタグに付けるclassName */
        order: 'list-order',
        /* messageBox出力につけるclassName */
        message: 'message-box'
    },
    url: {
        hogeIcon: 'images/path/to/hoge.gif',
        fooImage: 'images/path/to/thumb/foo.jpg',
        boxBar: 'images/path/to/style_bar.gif'
    }
    fadeOutTime: 100,
    focusInTime: 150,
    blurTime: 1000,
    emptyFunction: function(){}
};

HTML上表現可能なものは、HTMLテンプレートで定義しましょう。動的なものは設定値として定義しておくと便利です。
また、メッセージなども修正量が多いため、次のようにまとめておくのも良いです。

var MessageId = {
    msg001: '#{name}は必須入力です',
    msg002: '#{name}は#{from}から#{to}の間で入力してください',
    msg003: '#{0}は#{1}の入力でなければなりません'
};
var Regex = {
    folderAll: /#\{((\d|\w)+)\}/g,
    degit: /\d/g,
    alphaAll: /([a-zA-z].+)/g
};
var Message = {
    getMessage: function(msg, params){
        var place = arguments.callee.place;
        if(place == null){
            place = function(parameter){
                return function (target, match){
                    return parameter[match];
                };
            };
            // for singleton
            arguments.callee.place = place;
        };
        return msg.replace(Regex.folderAll, place(params));
    }
};

alert(Message.getMessage(MessageId.msg001, {name: 'Hoge'}));
alert(Message.getMessage(MessageId.msg002, {name: 'Foo', from: '9月', to: '11月'}));
alert(Message.getMessage(MessageId.msg003, ['2007/08/28', '2007/10/18']));

正規表現などは、複雑にならない限り一元化するまでも無いですが、良く使う正規表現はコード中各所に分散してしまいがちなので、 上記のようにある共通化された処理(実際に使っているのとは違いますが)のようにまとめておくことをお薦めします。

3. 分岐の高速化

画面の起動時(onload)などで判定可能なメソッドやその他処理は、実行時にif分岐をするのではなく、分岐後の値を定義しておくのがいいでしょう。
数が多い場合には少し辛いかもしれませんが、分岐の集約(?)は修正量を減らすことに役立ちます。

以下はあまりよろしくないコード

var Hoge = {
    methodA: function (){
        :
        : doSomething
        :
    },
    methodB: function (){
        :
        : doSomething
        :
    }
};

var LogicFoo = {
    onload: function (fieldHoge){
        if(fieldHoge.style.display == 'none'){
            Hoge.methodA(hoge);
        } else {
            Hoge.methodB(hoge);
        }
    }
}

Event.observe(window, 'load', function(){
    LogicFoo.onload(document.getElementById('hoge'));
});

ここでは、onloadのタイミングでid="hoge"のstyle値に依存しています。
このような処理は以下のような、実行可能インタフェースとして定義しておくことで、分岐を一ヶ所に集約することができます。

var Hoge = function(fieldHoge){
    if(fieldHoge.style.display == 'none'){
        this.execute = this.methodA;
    } else {
        this.execute = this.methodB;
    }
};
Hoge.prototype = {
    execute: function (){
        // no operation
    },
    methodA: function (){
        :
        : doSomething
        :
    },
    methodB: function (){
        :
        : doSomething
        :
    }
};

var LogicFoo = {
    onload: function (hoge, fieldHoge){
        hoge.execute(fieldHoge);
    }
}

Event.observe(window, 'load', function(){
    var fieldHoge = document.getElementById('hoge');
    var hoge = new Hoge(fieldHoge);
    LogicFoo.onload(hoge, fieldHoge);
});

javascriptの動的な部分を活かすことで、メソッドの再定義や(何度も書いてしまっている)interceptorが楽に記述できます。
また、再定義などをされたくない場合はローカルスコープ(クロージャとか)で隠蔽可能なため、それらを上手く活用します。

# 上のサンプルあくまでサンプルです。もっと集約することも可能でしょうし、もっといい方法もあります

4. 高速化のための設計

javascriptは他の言語と比べ、作りながら設計するスタイルが多いですが、あらかじめ、キチンと修正しやすいように設計することをお薦めします。
コーディングするとき、修正するときに、どこをどのように修正するか、設計されていると高速に開発することができます

また、設計する際には、javascriptの特性である、動的言語やprototype、ブラウザ毎の振る舞いの違い、ドメイン問題などを把握しておくことを強く強調します。
設計することに意味がありますが、Javaのような設計では逆に高速化から遠く離れてしまう可能性も無きにしもあらずです。

4.5 フレームワークを使う/作る

上記のように設計をしていると、ある一定のフレームワークが完成します。一定のフローを元に開発することで修正の手間は大きく減ります。

その為に必要なライブラリは、実際に使ってみて導入する検討材料にしましょう。
もし導入に十分なライブラリであっても、不足な機能があったとしても、そこはjavascriptの動的言語の特性を活かすことで不足な機能を補うことが出来ます。
また、ライブラリが多くなってきて、記述の差異(prototype.jsとjqueryみたいに凄い記述の差)がでてきた場合は、それらをラッピングできるフレームワークを作ります。

要するに、javascriptを組み合わせて如何に楽をするか(高速にモノを用意するか)が重要になります。

5. 思考の高速化

最近のjavascripterは覚えることが多くあります、クロージャや高階関数、Ajax。

ですが、そういう方知識が無いjavascripter(言葉は悪いですが、Web1.0時代の方や、oop以外の関数プログラマ)が多いことも忘れてはなりません。
よってサンプルコードを提示します、ドキュメントを残すことは必須です。(底上げを行いましょう)

以下は、jsDocを使ったサンプル

var HogeClass = Class.create();
HogeClass.prototype = {
    /**
     * コンストラクタ
     *
     * <pre><code>
     * var foo = new FooClass();
     * var values = ['090', '1234', '5678'];
     * var hoge = new HogeClass(foo, $('barId'), values);
     * :
     * :
     * :
     * hoge.validate('bazField');
     * </code></pre>
     *
     * @constructor
     * @param {FooClass} foo 使用するFooClass
     * @param {HTMLEelement} element ほげのinput
     * @param {Array} values <code>element</code>に入力する値のデフォルト配列
     * @see FooClass#joinLine
     */
    initialize: function(foo, element, values){
        element.value = foo.joinLine(values);
    },

    /**
     * @ignore ドキュメント出力を行わない
     */
    _getInputs: function (id){
        var result = [];
        var inputs = $(id).getElementsByTagName('input');
        inputs.each(function(input){
            if('text' == input.tagName.toLowerCase()){
                result.push(input);
            }
        });
        return result;
    },

    /**
     * 指定されたフィールドのId(<code>divFieldId</code>)以下のinputについてバリデート処理を行います。
     * @param {String} divFieldId validateするフィールドのID
     * @see #validateA
     * @see #validateB
     */
    validate: function (divFieldId){
        var inputs = this._getInputs(divFieldId);
        inputs.each((function(e){
            if(e.name == 'inputA'){
                this.validateA(e);
            }
            :
            : doSomething
            :
        }).bind(this));
    },

    /**
     * inputAについてvalidate処理を行います。
     * @private
     * @param {InputElement} inputA 入力項目
     */
    validateA: function (inputA){
        :
        : doSomething
        :
    },

    /**
     * inputBについてvalidate処理を行います。
     * @private
     * @param {InputElement} inputB 入力項目
     */
    validateB: function (inputB){
        :
        : doSomething
        :
    }
};

var FooClass = Class.create();
FooClass.prototype = {

    /**
     * 引数<code>array</code>を{@link Configure.line}で連結した文字列を返却します。
     *
     * <pre><code>
     * Configure.line = '-';
     * var strings = ['123', '456'];
     * var foo = new Foo;
     * alert(foo.joinLine(strings)); // 123-456
     * </code></pre>
     *
     * @param {Array} strings 連結する文字列が格納された配列
     * @return {@link Configure.line}で連結した文字列
     * @type String
     */
    joinLine: function(strings){
        return strings.join(Configure.line);
    },

    /**
     * 引数<code>array</code>を<code>target</code>で連結した文字列を返却します。
     *
     * <pre><code>
     * var strings = ['abcdefg', 'hogeFoo'];
     * alert(new Foo.join('&lt;br /&gt;', strings)); // abcdefg&lt;br /&gt;hogeFoo
     * </code></pre>
     *
     * @param {String} target 連結に使用する文字列
     * @param {Array} strings 連結する文字列が格納された配列
     * @return <code>target</code>で連結した文字列
     * @type String
     * @deprecated <code>target</code>に任意の文字列が使用可能な為、使用しなくなりました。
     */
    join: function(target, strings){
        throw new Error(221, '引数targetに任意の文字列が使用可能な為、使用しなくなりました');
//        return strings.join(target);
    }
}

ドキュメントが増えてくると、サンプルコートが無いとどうやって動かして良いのかわからなくなります。
jsDocはJavaDocと似たようなタグ(doclet)を使うことが可能なので、適切にドキュメントを書いておきましょう

6. 高速に作れるようにする(プロトタイプを使って作るモノを減らす)

実装を進めていくと、よく使う関数やオブジェクトが増えてきてしまいがちになります。
多くは、継承パターンを上手く活用していくことで、修正を少なくすることができます。javascriptにはprototypeという便利な継承方法があるため、これを使いましょう。

以下はこれまで紹介したメソッドなどをprototype化したもの

/**
 * 空白文字列を取り除きます
 *
 * <pre><code>
 * 'hoge foo bar'.trim(); // 'hogefoobar'
 * </code></pre>
 *
 * @type string
 * @return trim後の文字列
 */
String.prototype.trim = function(){
    return this.replace(/^\s+|\s+$/g, '');
};
/**
 * メッセージを生成します。
 *
 * <pre><code>
 * '#{name}は#{age}ですね'.createMessage({name: 'Hoge', age: 23});
 * MessageId.msg001.createMessage(['a', 'b', 'c']);
 * </code></pre>
 *
 * @param {Array} parameter メッセージ生成にしようする配列
 */
String.prototype.createMessage = function(parameter){
    return Message.getMessage(this, parameter);
};

Array.prototype.addAll = function(){
    for(var i = 0; i < arguments.length; ++i){
        this.push(arguments[i]);
    }
    return this;
};
Array.prototype.unshift = function(value){
    return [value].addAll(this);
};
Array.prototype.innerJoin = function(prefix, suffix, separator){
    var result = [];
    this.each(function(value){
        result.push(prefix + value + suffix);
    });
    return result.join(separator);
};
Array.prototype.outerJoin = function(prefix, suffix, separator){
    return prefix + this.join(separator || '') + suffix;
};

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

String.prototypeArray.prototypeを増やしてしまうと、for(..in..)で苦労する事になりかねます。(高速化しにくくなります)
ある一定までオブジェクトはラッピングしつつ、適切なプロトタイプを作成することを心がけます。

まとめ

javascriptは高速です。遅いところは遅くともそれをカバーできればこんなにも普及している言語は少なくないでしょう。
資料が多いのもまた良いです。
早く開発を終わらせて、さっさと帰宅できるようにしたいものです。

以下、高速化することによるメリット(の予想)

スタート
↓
設計
↓
高速に開発!
↓
高速に帰宅!
↓
合コンのチャンスUp!!
↓
高速にモテモテ!!!
↓
デート回数Up!
↓
女の子付き添いでカンファレンスとかにいける!
↓
「デキる!人」と思われる!!
↓
高速にゴール

Trackback

No Trackbacks

Track from Your Website

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

2 Comments

Re: Javascriptによる大規模開発の覚え書き。高速化編

合コンのチャンスUp!!

高速に情報漏れ!・・・orz
が抜けてますぜダンナ。

From : nejima @ 2007-10-08 22:05:58 編集

Re: Javascriptによる大規模開発の覚え書き。高速化編

>nejimaさん
たしかに、高速に 秘密な 合コンをしたのに
次の日、バレているあの現場の謎・・・orz

From : ハタ @ 2007-10-09 01:11:32 編集

Post Your Comment


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

1 + 2 =