関数型コンストラクタ 継承サンプル : JavaScript The Good Parts

JavaScript:The Good Parts」 5章-5.4 でプロトタイプ継承に代わる継承方法としてnew演算子を使わない関数型コンストラクタが載っています。そのサンプルコードも記載されていましたが、それだけではまだ実際のイメージがつかみにくい部分があるのと、プライベートなプロパティを設定して継承するというサンプルが無かったので、自分でプログラムサンプルを作成してみることにしました。

目次

  1. 関数型コンストラクタの定義
  2. 関数型コンストラクタの実行サンプル
  3. 継承した関数型コンストラクタの定義
  4. 継承した関数型コンストラクタの実行サンプル
  5. まとめ

1.関数型コンストラクタの定義

以下のような仕様をイメージして関数型コンストラクタを実装しました。
■関数型コンストラクタgaishoで生成されるオブジェクトは、以下のプロパティを保持している。[名前、年齢、体力、能力、ポイント(お金でも良かったんですがちょっと生々しいので・・(ё_ё))]
■寝る、働くの2つのメソッド(public)を保持している。

  • 「働く」によりポイントが増えていくが、働くことにより体力がマイナスされる。
  • 「寝る」により体力を回復をさせることができる。
  • 体力が0以下になると働けない
  • ポイントが100を超えるとメッセージ表示される

要するに、働くのと寝るのしかできない単純なオブジェクトです。ただ、イメージしやすいものということで実際の人に近いものにしてみました。

以下関数型コンストラクタのコードです。

var gaisho= function ( spec, my ){
    //@1-1 外からアクセス可能なプロパティ、メソッドを格納する
    var that = {};
    
    //@1-2 myには privateな変数・メソッドを格納     --//
    //継承されたコンストラクタで利用可能         --//
    my = my || {};
    my.addPowerCntAtSleep = 20;    //寝たときに体力に加算される数値
    my.addPowerNumAtWork = -15;    //働いたときに体力に加算される数値
    
    //働いた時のポイントの計算    
    my.calcWorkedPoint = function(){
        return Math.floor(
            (Math.floor(spec.age / 5) * spec.ability ) / 10);
    }
    
    //--以下public 外部からアクセス可能 --//
    
   //寝る
    that.sleep = function(){
        document.writeln('Sleeped zzzzzzz');
        spec.power += my.addPowerCntAtSleep;    //power回復
        document.writeln(that.toString());
    };
    
    //働く
    that.work = function(){
        var workedPoint, 
            pointLast = spec.point;
            
        //パワーが0以下の場合は働けない
        if(spec.power <= 0){
            document.writeln('No power .....');
            return;
        }

        //ポイントを計算
        workedPoint = my.calcWorkedPoint();
        document.writeln('Worked. Get '+ workedPoint + ' point.');
        
        spec.point += workedPoint;        //ポイント獲得
        spec.power += my.addPowerNumAtWork;    //パワーマイナス

        document.writeln(that.toString());
        
        //ポイントが100を超えた!
        if(pointLast < 100 && spec.point >=100){
             document.writeln('Over 100 point !');
        }
    };
   
    that.toString = function(){
        return '['+'name:' + spec.name 
                + ' age:' + spec.age
                + ' power:' + spec.power 
                + ' ability:' + spec.ability
                + ' point:' + spec.point + ']';
    }
    
    //@1-3
    return that;
};

コードを簡単に説明します。
引数specには、オブジェクトを生成されるのに必要な情報が渡ってきます。(ここでいうname,age,power,ablity,pointの各フィールドです)。
フィールドthat(@1-1)にはオブジェクトを生成して格納し、そのプロパティとして外部からアクセス可能(public)なメソッドやフィールドを格納してきます。今回設定されているpublicなメソッドは、sleep()メソッド、work()メソッドです。ついでに、自身のオブジェクトの情報を返すメソッドとしてtoString()メソッドも定義しておきます。一番最後の部分でthatはreturnされていますが(@1-3)、最終的に戻り値として返されるため、それを受け取った側はthatオブジェクトのプロパティ、メソッドにアクセスが可能となるわけです。

フィールドmy(@1-2)には、外側からはアクセスさせないが継承された場合に利用させたいフィールドやメソッドを格納しています。ここでは、メソッドcalcWorkedPoint()と、寝た時と働いた時に体力に加算される数値をmyに格納しています(my.addPowerCntAtSleep と my.addPowerNumAtWork )。

2.関数型コンストラクタの実行サンプル

ではこれを元に実行してみます。gaishoコンストラクタ関数よりオブジェクトを生成させて、頑張って働いてポイントを100まで稼ぎます。
※右側にコメントで出力結果をつけています。

//@1-3 関数型コンストラクタによりオブジェクト生成
var gaishoObj = gaisho(
    {name : 'gaisho1' , age : 31 , power : 50 , 
      ability : 30,  point :0}
);

//5回働く!
gaishoObj.work();	//Worked. Get 18 point.
                        //[name:gaisho1 age:31 power:35 ability:30 point:18]
							
gaishoObj.work();	//Worked. Get 18 point.
                        //[name:gaisho1 age:31 power:20 ability:30 point:36]

gaishoObj.work();	//Worked. Get 18 point.
                        //[name:gaisho1 age:31 power:5 ability:30 point:54]
							
gaishoObj.work();	//Worked. Get 18 point.
			//[name:gaisho1 age:31 power:-10 ability:30 point:72]
							
gaishoObj.work();	//No power .....


//2回寝る
gaishoObj.sleep();    //Sleeped zzzzzzz
                      //[name:gaisho1 age:31 power:10 ability:30 point:72]

gaishoObj.sleep();    //Sleeped zzzzzzz
                      //[name:gaisho1 age:31 power:30 ability:30 point:72]

//もう2回働く
gaishoObj.work();    //Worked. Get 18 point.
                     //[name:gaisho1 age:31 power:15 ability:30 point:90]
                            
gaishoObj.work();    //Worked. Get 18 point.
                     //[name:gaisho1 age:31 power:0 ability:30 point:108]
                     //Over 100 point !

5回目働こうとしたところで疲れ果ててしまいましたね。体力回復のため2回寝て、もう2回働きました。
これにより、無事に100ポイントを超えることができました!(なんじゃそりゃ)

※繰り返し処理はホントはループで処理させた方が良いのですが、わかりやすくするためあえて複数回呼び出しています

中身を説明しておきますと、まず最初のコンストラクタを利用したオブジェクト生成のところでオブジェクトに必要なフィールドの値をセットして引数として渡します(spec)。
そして後は外部から呼出し可能なメソッド(work()とsleep())を呼び出して操作するだけです。

ちなみにwork()が呼ばれたときに獲得するポイントは以下のように計算するようにしています。

  Math.floor(
      (Math.floor(spec.age / 5) * spec.ability ) / 10);


年齢(spec.age)を5で割って(小数点以下切り捨て)それに能力( spec.ability)をかけて10で割っています(再度小数点以下切り捨て)。かなり適当に思いついた計算なので中身はあまり気にしないでください・・。まあ、年齢とスキルにより、ある程度ポイント額が変わってくるわけです。実社会のごとく(笑)。

3.継承した関数型コンストラクタの定義

さて、gaishoコンストラクタは無事に動作しましたが、これを拡張する必要が出てきました(という想定)。以下の内容です。
■働いた時のポイントの計算方法の変更
現行の計算方法に処理を追加し、新たにランダムで生成した数値を加味するようにします。
■寝た時の体力の回復値を変更(20→30)
■ポイントが200を超えた時のメッセージの追加
現行のものは100ポイントを超えたタイミングでメッセージがでるが、200ポイントを超えた場合にもメッセージを出すように変更する
※ただし、既存で利用しているgaishoコンストラクタに影響を与えてはいけない
これらの内容を加味し、継承コンストラクタの実装を行おうと思いますが、その前に継承コンストラクタの実装で必要になる関数をあらかじめ定義しておきます。

//@3-1 全ての関数でメソッドを追加可能にする
Function.prototype.method = function ( name , func ){
     if (! this.prototype [ name ] ){
          this.prototype [ name ] = func ;
     }
      return this ;
};

//@3-2 Objectにsuperiorメソッドを追加
//継承元のオリジナルメソッドを呼び出すためもの
Object.method('superior', function (name){
    var that = this,
        method = that[name];
    return function () {
        return method.apply(that, arguments);
    }
});


※@3-1 Function.prototype.method = ... は「JavaScript: The Good Parts」4.7(P38)を参考に記述
※@3-2 Object.method('superior' ... は、「JavaScript: The Good Parts」5.4(P63)より引用

@3-1では、メソッドを追加するためのmethodメソッドを定義しています。標準APIのFunction.prototypeを拡張した場合、すべての関数で呼び出し可能になります。
@3-2では、定義したmethodメソッドをObject関数から呼出し、superior()というメソッドを追加しています。継承先で元あるメソッドをオーバーライドした場合に継承元メソッドを呼び出すために必要なメソッドになります。

では準備ができましたので継承コンストラクタの実装を行います。

var gaishoExtended = function(spec, my){
    my =  my || {};
    //@5-1 親コンストラクタによりthatオブジェクト生成
    var that = gaisho(spec , my);
        
    //@5-2 プライベートプロパティ拡張 眠って回復する数値を 20→30に変更
    my.addPowerCntAtSleep = 30;    
    
    //@5-3 プライベートメソッドを拡張
    my.calcWorkedPoint = function(){
        var randomInt = Math.floor(Math.random()*5);
            return Math.floor(
                (Math.floor(spec.age / 5) * spec.ability ) / 20) * randomInt;
    };

    //@5-4 継承元のworkメソッド呼び出しのための記述
    var super_work = that.superior('work');

    //@5-5 workメソッドをオーバーライド
    that.work = function(){
        var pointLast=spec.point;
        super_work();        //継承元のworkメソッドを呼び出し
        
        //新たに追加した処理
        //ポイントが200を超えた!
        if(pointLast < 200 &&  spec.point >=200){
            document.writeln('Over 200 point !');
        }
    };

    return that;
};


thatオブジェクトは親のコンストラクタを元に生成します(@5-1)。これにより継承元のメソッドはすでに存在する状態となります。このthatオブジェクトに対して新たな拡張部分の記述を行っていきます。

変数myに関しても、親のコンストラクタに引数として渡しているため(@5-1)、親(gaisho)で定義した部分(my.addPowerCntAtSleep,my.calcWorkedPoint()等)に関してはそのまま引き継がれます。よって新たに追加する部分、上書きする部分のみ記述を行っていきます。
@5-2では、my.addPowerCntAtSleepの値を変更し、眠った時に増える体力の数値を変更しています。
また、@5-3ではmy.calcWorkedPoint()を拡張し、働いた時のポイントの計算方法を変更しています。ちなみに、ここではランダムで0〜4の間の数値を生成し、それを今までの計算結果にかけています。つまり、働いたその時の運(気分、体調?)により、稼げるポイントが変わってくるという感じです。

次に@5-5でwork()メソッドをオーバーライドしています。ここでは親のwork()メソッドを呼び出した後に、新たにポイントが200を超えた時の処理を追加しています。親のwork()メソッドを呼び出すためには、@5-4の部分の記述が必要になります。superior()メソッドを利用して親のメソッドを呼び出せるようにしたため、継承したwork()メソッドでは既存の処理を変更することなく、新たに記述を追加するのみで拡張が行えました。

4.継承した関数型コンストラクタの実行サンプル

では継承したコンストラクタを元にオブジェクトを生成し、実行を行ってみます。今度はポイントが200溜まるまで働いてみます。※繰り返しが多いので今回はforループを使います

//オブジェクト生成
var gaishoObj2=gaishoExtended(
    {name : 'gaisho2' , age : 31 , power : 50 , 
      ability : 30,  point :0}
);

//5回働く
for(i=0;i<5;i+=1){
    gaishoObj2.work();
}
//出力結果
//Worked. Get 9 point.
//[name:gaisho2 age:31 power:35 ability:30 point:9]

//Worked. Get 27 point.
//[name:gaisho2 age:31 power:20 ability:30 point:36]

//Worked. Get 18 point.
//[name:gaisho2 age:31 power:5 ability:30 point:54]

//Worked. Get 9 point.
//[name:gaisho2 age:31 power:-10 ability:30 point:63]

//No power .....


//5回寝る
for(i=0;i<5;i+=1){
    gaishoObj2.sleep();
}
//出力結果
//Sleeped zzzzzzz
//[name:gaisho2 age:31 power:20 ability:30 point:63]
//Sleeped zzzzzzz
//[name:gaisho2 age:31 power:50 ability:30 point:63]
//Sleeped zzzzzzz
//[name:gaisho2 age:31 power:80 ability:30 point:63]
//Sleeped zzzzzzz
//[name:gaisho2 age:31 power:110 ability:30 point:63]
//Sleeped zzzzzzz


//もう7回働く!
for(i=0;i<7;i+=1){
    gaishoObj2.work();
}

//Worked. Get 18 point.
//[name:gaisho2 age:31 power:125 ability:30 point:81]

//Worked. Get 18 point.
//[name:gaisho2 age:31 power:110 ability:30 point:99]

//Worked. Get 36 point.
//[name:gaisho2 age:31 power:95 ability:30 point:135]
//Over 100 point !

//Worked. Get 9 point.
//[name:gaisho2 age:31 power:80 ability:30 point:144]

//Worked. Get 36 point.
//[name:gaisho2 age:31 power:65 ability:30 point:180]

//Worked. Get 27 point.
//[name:gaisho2 age:31 power:50 ability:30 point:207]
//Over 200 point !

//Worked. Get 36 point.
//[name:gaisho2 age:31 power:35 ability:30 point:243]

無事200ポイントを超えることが出来ました!
pointの加算も、数値がランダムに増えていっていることがわかります。
また、sleep()した時のpowerの数値が30ずつ増えるように変わっていることが確認できました。
めでたしめでたしo(^-^)o

5.まとめ

ということで、関数型コンストラクタとその継承のサンプルを一通り見てまいりました。
ちょっと無理やりなサンプルではありますが、関数型コンストラクタの継承の実際のイメージを少しはつかむことができたのではないかと思います。
特に私自身、本を読んだだけではmy変数の使い方等のイメージがはっきり掴めませんでしたが、実際に実装していくうちに徐々にクリアになってきました。

改めて実装方法を簡単に整理します。


  • インスタンス生成時に必要な情報は引数specにフィールドとして格納して渡す

  • 外部からアクセスさせたくないフィールド、メソッド(但し継承側で利用する)に関してはmyオブジェクトに追加する

  • 外部からアクセス可能(public)な特権メソッド、フィールドに関してはthatプロパティのオブジェクトに追加していく

  • プライベート変数(継承後も利用しない)に関してはmyでもthatでもなく、独立した変数として定義する

  • 継承した側でメソッドをオーバーライドしたが、親のメソッドを呼び出す必要がある場合はsuperior()を定義し呼び出せるようにする必要がある


更に従来のprototype継承ではなく、関数型コンストラクタを利用するメリットというのも見えてきました。今のところ実感したのは以下のようなメリットです。

  • アクセスさせたくないフィールド、メソッドを隠蔽することができる(これが一番の特徴ではないかと思います)→外部から不正に値が変更されることのない堅牢なオブジェクトを生成できる

  • 継承する際に、差分のみを記述すればよい→コードの再利用

  • 親のメソッドをオーバーライドした場合でも、元の親のメソッドを呼び出すことが可能

  • クラスの形式に比較的似ているため、Java等のクラス型のプログラムに慣れたプログラマーでも分かりやすい(prototype継承のnewを使う方がなじみやすいのかもしれませんが)


今後はさらに既存のprototype継承を利用して今回実装したものを実装し直してみたりしたいと思います。そうすることでprototype継承との違いがまた明確になるのでは・・と期待しています。

参考

http://d.hatena.ne.jp/kageroh_/20100702/1277969337
myフィールドの使い方で参考にしました
http://docs.komagata.org/4472
継承元のメソッド呼び出しで参考にしました