JavaScriptの設計について考える - 設計の応用例(canvas)

はじめに

本稿ではJavaScriptにおける弊社の基本的な設計方針についてご紹介しています。前回はEFOという比較的複雑な仕組みを例に挙げ、複数のオブジェクトを連携させて設計するための考え方について説明致しました。今回はもう1つの応用例として、canvasを使用した場合の設計方法について説明致します。

canvasとは

canvasはHTML5で新たに策定された仕様です。canvasはその名の通り何かを描画するためのキャンバスのようなものであり、javascriptで命令を与えることによって、どのような図形でも自由に描画することができます。Webページ上に何かを描画する場合、基本的にはDOMオブジェクトを生成することになります。しかしcanvasに描画した図形は単なるビットマップ情報として生成されるため、いくら図形を描画してもDOMオブジェクトの数は全く増えません。これは性能面では大きなメリットですが、プログラムによる制御の観点からするとデメリットにもなり得ます。

例えば特定の要素をアニメーションさせたりイベントハンドラを設定する場合、javascriptを使用してその要素に対応するDOMオブジェクトを操作することで実現することができます。しかしcanvasに描画した図形には対応するDOMオブジェクトが存在しないため、そもそも操作をすること自体ができません。これらの図形をアニメーションをさせるためには、図形自体の情報を変化させながら定期的に再描画を繰り返す必要があります。

そのためには描画した各々の図形の位置や大きさ等の情報を自力で管理しておく必要があります。この管理方法には、今までご紹介してきた設計の考え方を適用することができます。

canvasを使用したプログラム例

余り簡単なプログラムだと設計の必要性を感じることができませんし、複雑なプログラムだと処理の流れ自体が分かりにくくなってしまいます。そこで、以前に筆者がjsdo.itに投稿したプログラムを例に挙げて、canvasを使用した場合の設計の考え方について説明することにします。

このプログラムは行数も200行足らずの短いものであり、処理自体もそれほど複雑ではありません。しかし、それなりに設計をしっかりしないと、保守性が非常に低くなってしまう可能性があります。このプログラムは雪の舞い散る様子を表現しようとしたものですが、そのために次のような処理を実行しています。

  • 背景である夜空の色を徐々に変更する
  • 複数の雪の結晶を定期的に移動、回転、拡大させながら描画する

尚、各々の雪の結晶には次のような情報を持たせています。

  • 結晶の形
  • 表示位置
  • 大きさ

全ての雪の結晶の情報を1つのオブジェクトで一括して管理することもできますが、余程上手に設計しないと、データ構造と制御ロジックが複雑になります。このような場合はコンストラクタを定義して、雪の結晶とそれを制御するためのオブジェクトを1対1に対応させます。

そして前回説明したEFOと同様に、これらのオブジェクトを一括して管理するためのオブジェクトを定義し、連携させることによってシンプルに設計をすることができます。

設計方針

今回の例では次のように設計をしています。

  • 雪の結晶は、SNOWコンストラクタから生成したオブジェクトを割り当てて1対1で制御する
  • 雪の結晶の生成と描画、夜空の描画はRENDERオブジェクトで制御する

コードの詳細はjsdo.itの埋め込みコードを参照して頂くとして、ここではオブジェクトの大まかな枠組みと、ポイントとなる処理だけを説明します。

var RENDERER = {
    init : function(){
        this.setParameters();
        this.reconstructMethod();
        this.createSnow();
        this.render();
    },
    setParameters : function(){
        ........
    },
    /*
     * bindメソッドはjQueryの$.proxyと同じくthisの参照先を変更する機能を持つ。
     * javascriptのネイティプメソッドなので、$.proxyよりも高速。
     * 但しIE8はサポートしていないため、$.proxyとの使い分けが必要となる。
     * renderメソッドは何度も実行されるため、bindメソッドもその都度実行される。
     * しかしそれではオーバーヘッドが大きくなってしまう。
     * そこで高速化のため、予めrenderメソッドをbindの実行結果と置き換えておく。
     */
    reconstructMethod : function(){
        this.render = this.render.bind(this);
    },
    /*
     * 雪の結晶の数だけ、SNOWコンストラクタからオブジェクトを生成する。
     */
    createSnow : function(){
        for(var i = 0, length = this.SNOW_COUNT.INIT; i < length; i++){
            this.snows.push(new SNOW(this.width, this.height, this.center));
        }
    },
    render : function(){
        /*
         * requestAnimationFrameはモダンブラウザとIE10以上で使用可能なメソッド。
         * setTimeoutと同様、一定時間後に指定した関数を実行する。
         * setTimeoutと異なりブラウザが最適なタイミングで実行してくれる。
         */
        requestAnimationFrame(this.render);
        ........
        /*
         * 各SNOWオブジェクトに対して、自分の管理する雪の結晶を描画させる。
         */
        for(var i = 0, length = this.snows.length; i < length; i++){
            this.snows[i].render(this.context);
        }
        ........
    }
};
var SNOW = function(width, height, center){
    ........
    this.init();
};
SNOW.prototype = {
    init : function(){
        this.setParameters();
        this.createSnow();
    },
    setParameters : function(){
        ........
    },
    getRandomValue : function(range){
        ........
    },
    /*
     * 今回の仕様では、雪の結晶は1つずつ微妙に形を変えている。
     * しかし1度生成した結晶は、その後、形を変えることはない。
     * そこで初期化時に雪の結晶を描画したものを保存しておく。
     * 描画時には、その情報を再利用することで高速化を図る。
     */
    createSnow : function(){
        ........
    },
    /*
     * 初期化時に作成した雪の結晶の情報をコピーして描画する。
     * その際、描画内容を平行移動、回転、拡大することにより、動きを演出している。
     */
    render : function(context){
        ........
        context.translate(........);
        context.rotate(........);
        context.scale(........);
        context.drawImage(........);
        ........
    }
};
$(function(){
    RENDERER.init();
});

終わりに

今回の投稿を含めて、これまで5回に渡り弊社の設計方針と、その応用例を紹介させて頂きました。しかし紹介した2つの応用例は、まだまだ設計がシンプルな方です。複数のオブジェクトを連携させるための考え方は、まだまだ奥が深く、状況によってさまざまな考え方を使い分ける必要があります。

ご紹介した内容が、少しでも読者の方々の設計のヒントになれば幸いです。