カプセル化クロージャ

【JavaScript】 JSにおけるカプセル化手法

更新日:2021/02/01

オブジェクト指向プログラミングにはカプセル化という概念があります。
そこでJavaScriptにおけるカプセル化について考えてみます。

 

カプセル化とは

昔カプセル化について学んで、それを念頭に置いてプログラミングをしてきましたが、正確な概念についてはあいまいなところがありました。
そこで、少し整理しながら自分なりに解説してみます。

保守管理の容易化

カプセル化とは、ひとつまたは複数の処理と、その処理からのみアクセスできるデータをひとまとめにしたものです。
処理はインターフェース、つまり入力(引数)、目に見える動作(画面表示・ファイル操作など)、出力(戻り値)などを決めておきます。

こうすることで、処理そのものをブラックボックス化できます。

カプセル化 ブラックボックス化

そして、アルゴリズムやデータの所持形式を変更する必要があったとき、呼び出し元に影響を与えずに変更することが可能になります。
ようするに、外部のことを考慮しなくていいので、保守管理が容易になります。

この意味からすると、関数そのものが、コードがブラックボックス化されていて、カプセル化されていると言えます。

情報隠蔽

上記で「その処理からのみアクセスできるデータ」という記述をしています。
この意味をもう少し詳しく解説してみます。

次の例は、arg1とarg2を入力として受け取り、その合計を出力する関数で、その処理内容はカプセル化されています。


function func1(arg1,arg2){
   const sum = arg1 + arg2;
   return sum;
}

console.log( func1( 1 , 2 ) ); // 3
console.log( func1( 5 , 8 ) ); // 13

では次の例はどうでしょうか。


let sum = 0;

function func2(arg1){
   sum += arg1;
   return sum;
}

console.log( func2( 4 ) ); // 4
console.log( func2( 7 ) ); // 11
console.log( func2( 200 ) ); // 211

上のコードの関数func2は、呼び出されるたびに引数の和を記憶し、返しています。

このコードでは問題なく動作しますが、非常に大きな問題を抱えています。
変数sumが、関数func2以外から変更できてしまう点です。

つまり関数func2は、「引数で与えられた引数の和を返しているかどうか保証できない関数」になっているのです。
そうなると、関数func2の戻り値を検証する関数を作成することになります。
または、変数sumに値を代入するコードが紛れ込んでいないか、確認する必要があります。

非常に無駄な労力ですね。

そのため、この変数を外部からアクセスできないように隠蔽する必要があり、隠蔽した変数に対して「その処理からのみアクセスできるデータ」という表現をしているのです。

 

カプセル化の基本的な手段

多くのオブジェクト指向言語ではClass構文を使ってオブジェクトの設計をおこない、その中でデータを隠蔽するかどうかの定義を行います。

JavaScriptにはClass構文がありませんでしたが、最近になって導入されました。
しかし変数をオブジェクト内に隠蔽する構文は、未だに導入されていません。

ECMAScript2022からプライベート要素が導入されました。
これにより変数をオブジェクト内に隠蔽できるようになりました。
詳しくは次の記事で紹介しています。
【JavaScript】 クラス定義でプライベートプロパティが使用できるようになったと聞いて確認してみた

そのためクロージャの特徴を活かして、無理やりカプセル化するのが唯一の手法となっています。

クロージャによる情報隠蔽とカプセル化


function getFunc2(){

    let sum = 0;

    return function (arg1){
       sum += arg1;
       return sum;
    }
}

const func2 = getFunc2();

console.log( func2( 4 ) ); // 4
console.log( func2( 7 ) ); // 11
console.log( func2( 200 ) ); // 211

これにより、変数sumは返された関数以外から隠蔽されます。

 

オブジェクトとカプセル化

ここまでは関数を中心としたカプセル化について説明してきました。

次はオブジェクトをカプセル化してみます。

オブジェクトをカプセル化するコード

これまでのコードを、メソッドチェーンを考慮しながらオブジェクトに変更したものをカプセル化してみます。

オブジェクトとカプセル化


function getObj(){
    let sumValue = 0;

    const addFunc = function ( val ){
        if( !Number.isFinite(val) ) throw new Error("数字ではない!!");
        sumValue += val;
        return this;
    };

    const obj = {
        description:"数値を合計していくオブジェクト",
        add:addFunc,
        get value(){
            return sumValue;
        }
    };
    return Object.freeze(obj);
}
const obj = getObj();

console.log( obj.value ); // 0
console.log( obj.add( 4 ).add( 7 ).add( 200 ).value ); // 211

obj.addProp = 100; // エラー:Uncaught TypeError: can't define property "addProp": Object is not extensible
obj.description = 100; // エラー:Uncaught TypeError: "description" is read-only

※下から2行はstrictモード時に、エラースローされます。非strictモードでは動作が失敗しますが、エラースローされません。

上のコードは定数description、メソッドadd、ゲッターvalueをメンバーとしたオブジェクトを返しています。

最後のObject.freezeメソッドは、オブジェクトへのプロパティ追加と、プロパティの値の変更を禁止しています。
これにより、プロパティを定数化したり、メソッドを上書きするなどオブジェクトの改変ができなくなります。
ちなみに、変数をプロパティとして直接外部に公開できなくなるので、ゲッターとセッターが必須になります。

カプセル化は関数と変数の関係で考える

カプセル化は少し複雑なことをしようとすると、上手くいかなくなることが多いです。

そのため、常に『関数と変数の関係』を念頭においておく必要があります。

実は上のコードは、わざと冗長に書いてあります。
次のように、まとめることができます。


function getObj(){
    let sumValue = 0;

    return Object.freeze({
        description:"数値を合計していくオブジェクト",
        add:function ( val ){
            if( !Number.isFinite(val) ) throw new Error("数字ではない!!");
            sumValue += val;
            return this;
        },
        get value(){
            return sumValue;
        }
    });
}

ここで注目してほしいのが、addFuncとして定義していた関数コードを、オブジェクト内に直接記述している点です。

これはつまり、関数そのものの定義はオブジェクト内でも外でも関係がないということです。
重要なのは関数内で参照している変数が、変数が定義されている範囲(スコープ)内で定義されているかどうかで考える必要があります。

補足として、次の2つの失敗例を見てください。

やってしまいがちな失敗例1

例えば次のようなケースです。

動作検証のため隠蔽された変数の値をチェックしたい、しかしgetObjのコードを変更したくない、と考えました。
そこで、getObjで取得したオブジェクトに後からメソッドを追加しました。


function getObj(){
    let sumValue = 0;
    let count = 0;

    return Object.freeze({
        description:"数値を合計していくオブジェクト",
        add:function ( val ){
            if( !Number.isFinite(val) ) throw new Error("数字ではない!!");
            sumValue += val;
            count ++;
            return this;
        },
        get value(){
            return sumValue;
        }
    });
}

const obj = getObj();
  // デバッグ用のメソッドを追加
obj.debug = function(){ console.log( count ); }; // この位置からcountはスコープ外のため参照できない

上のコードで変数countは、オブジェクト内部のみで使用されていて、外部から完全に隠蔽されているとします。

この変数の値をチェックするために後から追加したdebugメソッドは、同じオブジェクトのプロパティなのだから、countにもアクセスできそうな気がします。
しかし function(){ console.log( count );} が記述されいる位置でスコープ解決されます。
この位置から見た変数countは、範囲外なのでアクセスできません。

やってしまいがちな失敗例2

もう一つ、やってしまいがちな失敗例を挙げてみます。

getObjの他に、現在の和を外部から変更できるオブジェクトを返す関数getObj2を作成しようと考えました。
ほぼコピペで大丈夫なのですが、sumメソッドの内容が同じになるのが美しくありません。
そこで関数の外部に出して、同じコードを参照させることにしました。


const isNumber = val => Number.isFinite(val);
const addFunc = function ( val ){
    if( !isNumber(val) ) throw new Error("数字ではない!!");
    sumValue += val;
    return this;
};

function getObj(){
    let sumValue = 0;

    return Object.freeze({
        description:"数値を合計していくオブジェクト",
        add:addFunc,
        get value(){
            return sumValue;
        }
    });
}

function getObj2(){
    let sumValue = 0;
    
    return Object.freeze({
        description:"数値を合計していくオブジェクト その2",
        add:addFunc,
        get value(){
            return sumValue;
        },
        set value(val){
            if( !isNumber(val) ) throw new Error("数字ではない!!");
            sumValue = val;
        }
    });

}

完全に同じコードが複数ある場合、その一つで何か不具合があったら、他の同じコードもチェックする必要があります。
しかし同じコードを呼び出すようにしておけば、その手間を減らすことができます。

そのため、上のコードのように変更してしまうことはよくあることです。

しかし失敗例1と同様に、 呼び出される関数内で使用されている変数sumValueはスコープ解決できないため、エラーになります。

共通部分をまとめる必要があるときは、関数内から呼び出す等の工夫が必要です。


    return {
        add:function(){
                sumValue = 共通関数(sumValue);
             }

 

カプセル化と即時関数

時々、カプセル化は即時関数でおこなうと説明されることがありますが、即時関数は必須ではありません。

まずは、これまでの例を即時関数で書き換えてみます。

即時関数による情報隠蔽とカプセル化


const obj = (function (){
    let sumValue = 0;
 
    return Object.freeze({
        description:"数値を合計していくオブジェクト",
        add:function ( val ){
            if( !Number.isFinite(val) ) throw new Error("数字ではない!!");
            sumValue += val;
            return this;
        },
        get value(){
            return sumValue;
        }
    });
})();

console.log( obj.value ); // 0
console.log( obj.add( 4 ).add( 7 ).add( 200 ).value ); // 211

即時関数を使用したことで、これまで定義してきたgetObjが不要になりました。
冗長だったコードを最適化したともいえます。

この型のオブジェクトを、プログラム上一つだけで運用していくなら、これで問題ありません。

しかし、次のように複数作成したいときは、関数として定義しておく必要があります。


const obj1 = getObj();
const obj2 = getObj();

即時関数を使用するかどうかは、オブジェクトの使用目的に合わせて検討していくということです。

 

プロトタイプやclass構文でのカプセル化

プロトタイプやclass構文のメソッドから、インスタンス毎にカプセル化した変数にアクセスしたい場合、少し工夫する必要があります。

詳しくは次のページを読んでみてください。

【JavaScript】 JSにおけるprivate変数と定義のひな型パターン
【JavaScript】 class構文でのprivate変数定義のひな型パターン

更新日:2021/02/01

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。