JavaScriptの設計について考える - 分類した機能の保守性を高める(後編)

CAROUSELオブジェクトを常に参照する変数を定義しておく

コードその11

LEIHAUOLI.SAMPLE_CODE.CAROUSEL = {
    .................
    setParameters : function(){
        this.$carouselContainer = $('#jsi-carousel-container');
        this.$previousTrigger = $('#jsi-previous-trigger');
        ................
    },
    bindEvent : fucntion(){
        var myself = this;
        this.$previousTrigger.on('click', function(){
             // カルーセルを1つ前に移動させる処理
             myself.$carouselContainer.aniamte({left : ....});
             ..........................
        });
    },
    ..................
};

上記の例ではコールバック関数に入る前に、thisの値をbindEventメソッドのローカル変数(myself)に代入しています。ローカル変数は上書きされない限り参照先が変わることはありません。よってコールバック関数内ではthisの代わりにmyselfを使用することで、CAROUSELオブジェクトを参照することができます。

コールバック関数内のthisの参照先をCAROUSELオブジェクトに変更する

jQueryでは、関数内で使用するthisの参照先を変更するための機能として、次のようなメソッドを提供しています。

$.proxy(関数, 関数内でthisが参照するオブジェクト, 関数の引数1, 関数の引数2,...)

$.proxyを実行すると、最初の引数で指定した関数がそのまま返ってきます。その際その関数には、2番目の引数に指定したオブジェクトが紐付けられます。簡単な例を挙げましょう。

コードその12

var greetingObject = {
    say : 'hello'
}
var greeting = function(){
    return this.say;
}
var boundGreeting = $.proxy(greeting, greetingObject);
console.log(boundGreeting()); // コンソールにhelloと表示される

関数greetingのように、グローバルに定義した関数はwindowオブジェクトのメソッドになるため、thisは本来windowオブジェクトを参照します。しかし$.proxyを利用してgreetingObjectと紐付けることにより、thisがgreetingObjectを参照するようになります。その結果、コンソールにはhelloと表示されることになります。

よって次のように記述することにより、コールバック関数内のthisがCAROUSELオブジェクトを参照するように変更することができます。

コードその13

LEIHAUOLI.SAMPLE_CODE.CAROUSEL = {
    .................
    setParameters : function(){
        this.$carouselContainer = $('#jsi-carousel-container');
        this.$previousTrigger = $('#jsi-previous-trigger');
        ................
    },
    bindEvent : fucntion(){
        this.$previousTrigger.on('click', $.proxy(function(){
             // カルーセルを1つ前に移動させる処理
             this.$carouselContainer.aniamte({left : ....});
             ..........................
        }, this));
    },
    ..................
};

さて、今回の例に挙げたカルーセルの場合、機能を構成する処理の種類がそれ程多くないため、この程度までメソッドに分割することができれば十分でしょう。しかし複雑な機能になると更なる分割が必要になります。Webサイトの機能をJavaScriptで実装する場合、最も複雑になる可能性が高いのは、イベントハンドラを始めとするコールバック関数の処理です。そのような場合、1つのコールバック関数を複数のメソッドに分割することもあります。

コールバック関数をメソッド化する

今回の例ではイベントハンドラは2つしか設定していません。また具体的な処理を端折っていますが、各イベントハンドラの処理は比較的短く記述するこができるでしょう。しかし設定するイベントハンドラの数が多くなり、かつイベントハンドラ自身の処理が複雑になった場合、bindEventメソッドが肥大化することになります。このメソッドは、その名前の通りイベントハンドラの登録をするだけに留め、イベントハンドラの中身は独立したメソッドとして定義した方がよいでしょう。

このようにイベントハンドラを独立させることにより、コード全体の見通しがよくなると共に、イベントハンドラを再利用することができるようになります。例えばカルーセルでは多くの場合、定期的に移動させる処理と、ユーザが「前へ」または「次へ」ボタンを押下した時の処理は等しくなります。この点を踏まえた設計を次に示します。

コードその14

var LEIHAUOLI = LEIHAUOLI || {};
LEIHAUOLI.SAMPLE_CODE = {};

LEIHAUOLI.SAMPLE_CODE.CAROUSEL = {
    SLIDE_INTERVAL : 5000,

    init : function(){
        this.setParameters();
        this.prepareToExecuteCarousel();
        this.bindEvent();
        this.setTimer();
    },
    setParameters : function(){
        this.$carouselWrapper = $('#jsi-carousel-wrapper');
        this.$carouselContainer = $('#jsi-carousel-container');
        this.$carouselElements = $carouselContainer.children('li');
        this.$previousTrigger = $('#jsi-previous-trigger');
        this.$nextTrigger = $('#jsi-next-trigger');
    },
    prepareToExecuteCarousel : function(){
         ..........................
    },
    bindEvent : function(){
         this.$previousTrigger.on('click', $.proxy(this.moveToPrevious, this));
         this.$nextTrigger.on('click', $.proxy(this.moveToNext, this));
    },
    setTimer : function(){
        setInterval($.proxy(this.moveToNext, this), this.SLIDE_INTERVAL);
    },
    moveToNext : function(){
        ...........................
    },
    moveToPrevious : function(){
        ...........................
    }
};

$(function(){
    LEIHAUOLI.SAMPLE_CODE.CAROUSEL.init();
});

コードその14ではイベントハンドラを始めとするコールバック関数をメソッド化しています。これにより、$nextTriggerのイベントハンドラとsetIntervalのコールバック関数を共通化することができるようになり、コード全体の見しが更に良くなりました。

これに加えてコード内に直接記述していたタイマーの設定時間を、インスタンス変数(SLIDE_INTERVAL)として定義し直しています。この変数は他のインスタンス変数と異なり、オブジェクトの初期値として定義しています。勿論、他のインスタンス変数同様、オブジェクト実行時に定義しても問題ありません。これは弊社の考え方、というより筆者の考え方なのですが、この値は変数ではなく定数としての意味合いで定義しています。よって他の変数との差別化を行うために、変数名を大文字にし、かつ定義方法を変えることにより、定数であることが一目で分かるようにしています。尚、コード内に埋め込まれた値を定数として定義することにより、次のようなメリットが得られます。

  • 適切な定数名をつけることにより、値の持つ意味が明確になる
  • 定数によって値を一元管理しているため、値の変更を容易に行うことができる

クロージャとの併用

今回は弊社のオブジェクト設計の考え方について紹介しましたが、設計の考え方は他にも色々あります。よく用いられるのが、オブジェクトとクロージャを組み合わせる方法です。次に示す例は、インスタンス変数を排除して、クロージャのローカル変数として定義したものです。

コードその15

var LEIHAUOLI = LEIHAUOLI || {};
LEIHAUOLI.SAMPLE_CODE = {};

LEIHAUOLI.SAMPLE_CODE.CAROUSEL = function(){
    var SLIDE_INTERVAL = 5000,
        $carouselWrapper = $('#jsi-carousel-wrapper'),
        $carouselContainer = $('#jsi-carousel-container'),
        $carouselElements = $carouselContainer.children('li'),
        $previousTrigger = $('#jsi-previous-trigger'),
        $nextTrigger = $('#jsi-next-trigger');

    return {
        init : function(){
            this.prepareToExecuteCarousel();
            this.bindEvent();
            this.setTimer();
        },
        prepareToExecuteCarousel : function(){
             ..........................
        },
        bindEvent : function(){
             $previousTrigger.on('click', $.proxy(this.moveToPrevious, this));
             $nextTrigger.on('click', $.proxy(this.moveToNext, this));
        },
        setTimer : function(){
            setInterval($.proxy(this.moveToPrevious, this), this.SLIDE_INTERVAL);
        },
        moveToNext : function(){
            ...........................
        },
        moveToPrevious : function(){
            ...........................
        }
    };
};

$(function(){
    LEIHAUOLI.SAMPLE_CODE.CAROUSEL().init();
});

この書き方の特徴は、オブジェクト外部に公開したくない情報を完全に隠ぺいすることができることです。例えば$carouselWrapperを始めとするインスタンス変数は、オブジェクト外から参照する必要はありません。むしろ参照可能なことにより、値を書き換えられる危険性が存在します。上記のように隠ぺいしてしまえば、間違っても書き換えられる可能性はなくなるため、より堅牢な設計をすることができます。これは明確なメリットです。

もう1つの特徴は、インスタンス変数を排除したことにより、変数参照時にthisキーワードを使用する必要がなくなったため、より簡潔にコードを記述できるようになるということです。しかし筆者はこれは明確なメリットとは考えていません。確かに簡潔に記述することができますが、その代わり、複数のメソッドで共有している変数が不明瞭になるため、処理の流れが分かりにくくなるからです。

コードその15の考え方を更に押し進め、インスタンス変数だけではなく、外部に公開する必要のないメソッドもクロージャのローカル関数として定義する方法もあります。

コードその16

LEIHAUOLI.SAMPLE_CODE.CAROUSEL = function(){
    var SLIDE_INTERVAL = 5000,
        $carouselWrapper = $('#jsi-carousel-wrapper'),
        $carouselContainer = $('#jsi-carousel-container'),
        $carouselElements = $carouselContainer.children('li'),
        $previousTrigger = $('#jsi-previous-trigger'),
        $nextTrigger = $('#jsi-next-trigger');

    var init = function(){
        prepareToExecuteCarousel();
        bindEvent();
        setTimer();
    },
    prepareToExecuteCarousel = function(){
         ..........................
    },
    bindEvent = function(){
         $previousTrigger.on('click', moveToPrevious);
         $nextTrigger.on('click', moveToNext);
    },
    setTimer : function(){
        setInterval(moveToPrevious, this.SLIDE_INTERVAL);
    },
    moveToNext = function(){
        ...........................
    },
    moveToPrevious = function(){
        ...........................
    };
    return {
        init : init
    };
};

$(function(){
    LEIHAUOLI.SAMPLE_CODE.CAROUSEL().init();
});

コードその16の場合、thisを完全に排除することができるため、より簡潔な記述が可能です。では設計方法としてはコードその16の記述方法が一番良いのでしょうか?確かにメリットも多いですし、この書き方が一番良いと考える人も少なくないかもしれません。尚、説明の便宜上コードその15やコードその16のようにクロージャを併用した設計をクロージャ設計と呼ぶことにします。

Webサイトの機能を実装する場合、同じ機能をページ内の複数の箇所に適用しなければならない状況が発生するることが少なくありません。今回の例で言えば、1つのWebページ内に複数のカルーセルが存在する場合がそれに当たります。このような場合、通常はコンストラクタと呼ばれる仕組みを利用します。

クロージャ設計は適用対象が1つしかない場合は良いのですが、複数存在する場合は実行効率が悪くなります。これはオブジェクト設計にも同じことが言えます。よって、オブジェクトやクロージャで設計した機能を複数の対象に適用する場合は、コンストラクタに変更する必要があります。そこで重要になるのが、両者の設計からコンストラクタに変更するための手間がどれだけかかるかということです。

結論から言うと、オブジェクト設計からコンストラクタに変更するのは一瞬でできます。何故ならば両者は構造がほぼ同じだからです。それに対してクロージャ設計からコンストラクタに変更するのは非常に大変です。

コンストラクタ化の手間とコード全体の統一性を考慮した場合、クロージャ設計の方が優れているとは一概に言えなくなります。弊社ではコード全体の統一性を重視するメンバが多いため、コンストラクタ設計よりオブジェクト設計を採用することが多くなっています。

終わりに

今回はオブジェクト設計の考え方について紹介しました。オブジェクト設計は、適用対象が1つの場合は処理の流れが明確になり、保守性の高いコードを記述することができます。しかし適用対象が複数になると、処理の流れが複雑になる可能性が高くなります。このような場合はコンストラクタを用いることで機能を簡潔に記述することができるようになります。次回は、このコンストラクタを使用した設計方法についてご紹介する予定です。

関連記事