JavaScriptの設計について考える - 機能ごとに分類する

はじめに

JavaScriptは、WebページにユーザインタラクティブなUIを実装するためのプログラミング言語です。この言語は、文法が単純であるため、プログラミング経験のない初心者でも、比較的容易に学習することができます。JavaScriptのフレームワークの1つであるjQueryの出現により、学習コストは更に下がりました。

Webが普及し始めた頃は、WebサイトのコンテンツやUIは非常にシンプルでした。よってJavaScriptで実装する機能も、多くの場合、ごく単純なものに限られました。しかしブラウザの表現能力の向上によって、リッチなWebサイトを構築することができるようになると、JavaScriptの役割も大きく変わってきました。それまでの補助的な役割から、サービスを実現するために欠かせないものになりました。HTML5で、新たにcanvasやインラインSVG、indexed DBなどが仕様として策定されると、この傾向に拍車がかかりました。

実装する機能が複雑になり、開発規模が大きくなると、プログラムの保守性や拡張性が重要になります。そのためには機能ごとにプログラムを適切に分類するための設計が必要になります。ところがJavaScriptに関しては、この設計が疎かにされるケースが非常に多いように思われます。

弊社ではJavaScriptに対しても設計の指針を設け、保守性と拡張性を担保するよう努めています。本技術ブログにおいて、何回かに分けて弊社の設計方針についてご紹介する予定です。

設計をせずに記述するデメリット

例えば、あるWebページに対してカルーセルとスライダーの機能を実装するとしましょう。設計を考えずに実装した場合、次のように記述することが考えられます。

記述形式その1

/*
 * カルーセルに必要な処理
 */
var variableA1 = xxx;
var variableA2 = yyyy;
...................
var operationA1 = function(){
    ...................
};
var operationA2 = function(){
    ...................
};
...................
/*
 * スライダーに必要な処理
 */
var variableB1 = xxx;
var variableB2 = yyyy;
...................
var operationB1 = function(){
    ...................
};
var operationB2 = function(){
    ...................
};
...................

上記のコードは整然と記述されているため、一見、保守性が高いように見えます。しかしこのコードには2つのデメリットがあります。

  • グローバル変数を多用しているため、他のファイルに定義したグローバル変数と名前の競合が発生する可能性が高い。
  • 機能ごとに物理的に分類されていないため、プログラムの修正や追加を行った場合の影響範囲が分かりにくい。

 グローバル変数の排除

ファイル間での変数名の競合の可能性を抑えるためには、グローバル変数の数を減らせば良いのです。次のように記述することで、グローバル変数の数を1つだけにすることができます。

記述形式その2

var wrapper = function(){
    /*
     * カルーセルに必要な処理
     */
    var variableA1 = xxx;
    var variableA2 = yyyy;
    ...................
    var operationA1 = function(){
        ...................
    };
    var operationA2 = function(){
        ...................
    };
    ...................
    /*
     * スライダーに必要な処理
     */
    var variableB1 = xxx;
    var variableB2 = yyyy;
    ...................
    var operationB1 = function(){
        ...................
    };
    var operationB2 = function(){
        ...................
    };
    ...................
};
wrapper();

コード全体をラッパー関数(上記の例ではwrapperという名前の関数)内に記述することにより、wrapper以外の変数をローカル変数にしています。更に次のようにラッパー関数に名前をつけずに即時実行すれば、グローバル変数を完全に排除することもできます。

記述形式その3

function(){
    /*
     * カルーセルに必要な処理
     */
    var variableA1 = xxx;
    var variableA2 = yyyy;
    ...................
    var operationA1 = function(){
        ...................
    };
    var operationA2 = function(){
        ...................
    };
    ...................
    /*
     * スライダーに必要な処理
     */
    var variableB1 = xxx;
    var variableB2 = yyyy;
    ...................
    var operationB1 = function(){
        ...................
    };
    var operationB2 = function(){
        ...................
    };
    ...................
})();

即時実行する場合は、上記のように無名関数の両端を()で括らないと文法エラーになります。これは、行頭が予約語functionから始まる場合、無名関数ではなく、関数宣言だと解釈されてしまうためです。関数宣言は次のように記述します。

function operation(){
    ........
}

これに対して次のように、無名関数を変数に代入する定義方法を関数式と呼びます。

var operation = function(){
    ........
};

関数宣言は関数式と異なり、予約語functionと()の間に関数名を指定する必要があります。文法エラーになるのは、関数宣言だと解釈されたにも関わらず、この関数名が指定されていないからです。

尚、この記述方法ではグローバル変数を排除することはできていますが、機能ごとに分類することができていません。機能ごとに分類するためには、オブジェクトを利用します。

機能ごとの分類

オブジェクトとは、関連性のあるデータや関数をまとめるための箱のようなものです。ある機能を実装するために必要なデータや関数を1つのオブジェクトの中にまとめることにより、次のようなメリットを得ることができます。

  • 各機能を明確に分類することができる。
  • 機能修正や追加する際の影響範囲をオブジェクト内に抑えることができる。

尚、オブジェクト内にまとめたデータをインスタンス変数、関数をメソッドと呼びます。オブジェクトは多くのプログラミング言語でサポートしている概念ですが、その仕組みは言語ごとに異なります。JavaScriptでは連想配列を、そのままオブジェクトとして利用しています。

「記述形式その3」をオブジェクトを利用して分類すると次のようになります。

記述形式その4

function(){
    var carousel = {
        variableA1 : xxx,
        variableA2 : yyyy,
        ...................
        operationA1 : function(){
            ...................
        },
        operationA2 = function(){
            ...................
        },
        ...................
    };
    var slider = {
        variableA1 : xxx,
        variableA2 : yyyy,
        ...................
        operationA1 : function(){
            ...................
        },
        operationA2 : function(){
            ...................
        },
        ...................
    };
})();

コメントがなくても、どのデータや関数がカルーセルに属するのか、またはスライダーに属するのか一目で分かります。これに加えて変数名や関数名を適切につければ、変数の役割や、関数の処理内容も直感的に理解できるようになるはずです。弊社では「コメントがなくても機能と構造が理解できるプログラム」を目指しています。

scriptファイルを分割した場合の問題点

Webサイトが複数のページで構成される場合、ページごとに必要な処理が異なることが少なくありません。その場合、各ページごとにscriptファイルを分割することになります。例えばトップページならばtop.js、詳細ページならばdetail.jsのようなファイル名にすることが考えられます。更に、多くのページで共通する処理も、1つのファイルにまとめることが多いと思います。この場合はcommon.jsのようなファイル名にすることが多いでしょう。

このように分割した場合、例えばトップページのHTMLには次のようにscriptタグを読み込むことが想定されます。

<html>
    <head>
        ........
    </head>
    <body>
        ........
        <!--自社のscript -->
        <script type="text/javascript" src="common.js"></script>
        <script type="text/javascript" src="top.js"></script>
        <!--他社のscript -->
        <script type="text/javascript" src="http://xxx/other.js"></script>
    </body>
</html>

上記のように自社のscriptに加えて、他社のscriptを読み込むことは、よくあることだと思います。

common.jsとtop.jsを「記述形式その4」のように記述した場合、各々のファイルのコードは、他のファイルから全く参照することができなくなります。勿論、それでも問題なければ良いのですが、場合によっては例えばtop.jsとcommon.jsが互いに一部の機能を参照したいということもあるかもしれません。

このような場合、次のように敢えてコード全体を関数で囲まずに記述することが考えられます。

記述形式その5

var carousel = {
    variableA1 : xxx,
    variableA2 : yyyy,
    ...................
    operationA1 : function(){
        ...................
    },
    operationA2 = function(){
        ...................
    },
    ...................
};
var slider = {
    variableA1 : xxx,
    variableA2 : yyyy,
    ...................
    operationA1 : function(){
        ...................
    },
    operationA2 = function(){
        ...................
    },
    ...................
};

この形式でも、オブジェクト内のインスタンス変数やメソッドが他のscriptファイルのコードと競合することはありません。しかしオブジェクト名はグローバル変数として定義しているため、競合する可能性があります。上記の例ではグローバル変数は2つしかありませんが、作成する機能の数が多くなればオブジェクトの数も増え、競合する可能性が高くなります。

名前空間の設定

名前空間は次のような特徴を持った概念です。

  • 同一の名前空間の中では、同じ名前の存在は許されない
  • 名前空間が異なる場合は、同じ名前の存在が許される

要するに自社のコードを1つの名前空間で囲んでしまえば、他社がどのような名前のグローバル変数を定義しても名前の競合が発生しないということです。残念なことにJavaScriptには名前空間を規定するための仕組みが存在しませんが、連想配列を利用することで疑似的な名前空間を作ることができます。

まずはscriptファイル単位で名前空間を作ることにします。この名前空間さえ競合しなければ、scriptファイル間で変数名が競合することはなくなります。common.js内のコードをCOMMONという名前空間で、top.js内のコードをTOPという名前空間で囲みます。

記述形式その6(common.js)

var COMMON = {};
COMMON.COMMON_OBJECT = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};
記述形式その6(top.js)

var TOP = {};
TOP.TOP_OBJECT1 = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};
TOP.TOP_OBJECT2 = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};

「記述形式その6」をご覧になれば分かるように、名前空間で囲むということは、連想配列の中にオブジェクトを配置するということです。JavaScriptでは、連想配列はオブジェクトとして利用することも、名前空間として利用することもできるのです。

更に自社のscriptファイルのコード全体をLEIHAUOLI_PROJECTという名前空間で囲みます。
「_」の前は自社名、後ろはプロジェクト名にすることにより、名前空間自身の競合の可能性をできる限り小さくするようにしています。

記述形式その7(common.js)

var LEIHAUOLI_PROJECT = {};
LEIHAUOLI_PROJECT.COMMON = {};

LEIHAUOLI_PROJECT.COMMON.COMMON_OBJECT = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};
記述形式その7(top.js)

LEIHAUOLI_PROJECT.TOP = {};

LEIHAUOLI_PROJECT.TOP.TOP_OBJECT1 = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};
LEIHAUOLI_PROJECT.TOP.TOP_OBJECT2 = {
    variable1 : xxxxx,
    variable2 : yyyyy,
    operation1 : function(){
        ........
    },
    operation2 : function(){
        ........
    }
};

これにより他社がLEIHAUOLIPROJECTという名前のグローバル変数を定義しない限り、変数名が競合することはなくなります。また、トップの名前空間から辿ることによって、異なるscriptファイルに記述したオブジェクトの機能を利用することができます。例えばtop.jsからcommon.jsで定義したCOMMONOBJECTオブジェクトのoperation1メソッドを実行する場合は、次のように記述します。

LEIHAUOLI_PROJECT.COMMON.COMMON_OBJECT.operation1();

但し「記述形式その7」には問題があります。それは、名前空間のLEIHAUOLIPROJECTがcommon.jsに定義されているため、scriptタグを読み込む順番を、common.js、top.jsの順番にしなければならないということです。更にcommon.jsが必要なくなった場合、top.jsに名前空間の定義を追加しなければならなくなります。かと言って全てのscriptファイルにLEIHAUOLIPROJECTを定義したら、scriptファイルを読み込むたびに名前空間を初期化してしまいます。

これではお世辞にも保守性が良いとは言えません。これを解決するためのちょっとしたテクニックがあります。それは全てのscriptファイルの先頭に次のような定義を記述することです。

var LEIHAUOLI_PROJECT = LEIHAUOLI_PROJECT || {};

scriptファイルを初めて読み込んだ時には、LEIHAUOLIT_PROJECTはundefinedになります。その結果、上記の定義は次の定義と等価になります。

var LEIHAUOLI_PROJECT = undefined || {};

JavaScriptではundefinedはfalse扱いになります。よって||の右側が評価されてLEIHAUOLIPROJECTに{}が代入され、正しく初期化されることになります。一方、二番目以降にscriptファイルを読み込んだ時には、LEIHAUOLIPROJECTには既にオブジェクトが代入されています。よって||の右側は評価されず、左側の値が代入されることになります。即ち、

var LEIHAUOLI_PROJECT = LEIHAUOLI_PROJECT;

と等価になるため、名前空間を初期化されることはなくなります。これにより順番を気にせずにscriptファイルを読み込むことができるようになります。

終わりに

今回はオブジェクトを利用して機能を分類するための方法について紹介しました。次回はオブジェクト自体の設計方針について紹介する予定です。

関連記事