日本野望の会 » level☆☆☆☆ http://yabooo.org いずれにせよITで世界征服をたくらむ悪の組織です。 Mon, 28 Dec 2009 14:30:41 +0000 http://wordpress.org/?v=2.8.3 ja hourly 1 Javascriptでカプセル化のコスト http://yabooo.org/archives/70 http://yabooo.org/archives/70#comments Thu, 07 Feb 2008 12:28:58 +0000 http://www.yabooo.org/archives/70 どうも、ひろきのだいちです。

Javascriptでカプセル化を実現する!の続編みたいなものです。

この記事を投稿したところ、chokodogさんから次のような指摘を受けました。
(コメントありがとうございます><)

カプセル化した場合、コンストラクタ内でメソッドが定義されているので、new するたびfunctionが定義され、prototypeで定義するよりメモリを多くとられてしまうということはないのでしょうか?

たしかにそのとおりです。

今回はカプセル化を先のような方法で行うとどのようなコストがかかるのかということ探ってみました。

メモリを使うのでは?ってどういうこと?

chokodogさんの指摘の意味を簡単に説明したいと思います。

まずは以下のjavascriptのプログラムを見てください。

JAVASCRIPT:
  1. //オブジェクトのプロトタイププロパティを設定
  2. Object.prototype.objprop=100;
  3. //Classオブジェクトの作成
  4. var Class=function(){};
  5. //Classのインスタンスオブジェクトの作成
  6. var instance=new Class();
  7. //Classプロトタイププロパティを設定
  8. Class.prototype.classprop=200;
  9. //instanceの
  10. instance.insprop=300;
  11.  
  12.  
  13. log(instance.objprop);
  14. //-->100
  15. log(instance.classprop);
  16. //-->200
  17. log(instance.insprop);
  18. //-->300
  19. log(instance.hasOwnProperty("objprop"));
  20. //->false
  21. log(instance.hasOwnProperty("classprop"));
  22. //->false   
  23. log(instance.hasOwnProperty("insprop"));
  24. //->false
  25. /*
  26.  ここでinstanceのプロトタイプチェーンをたどって
  27.  みつかったプロパティをOwnプロパティとして設定
  28. */
  29. instance.objprop=instance.objprop;
  30. instance.classprop=instance.classprop;
  31.  
  32. log(instance.hasOwnProperty("objprop"));
  33. // -->true
  34. log(instance.hasOwnProperty("classprop"));
  35. // -->true
  36. log(instance.hasOwnProperty("insprop"));
  37. // -->true

JavaScriptは通常のクラスベースのオブジェクト指向とことなりプロトタイプベースという別のプログラムパラダイムを採用しています。
JavaScriptのオブジェクトはすべてハッシュのようなものになっていますが、これに少しばかり細工がしてあります。

それがプロトタイプチェーンというやつです。

オブジェクトは、自身のプロパティで見つからないようなハッシュ値を自分自身のコンストラクタのプロトタイプへと探しにいきます。

たとえば、上のプログラムの場合、
objpropというプロパティを探しにいったときに

・自分自身のプロパティではない
・自分のコンストラクタのプロトタイププロパティではない。
・チェーンの終端であるオブジェクトのプロトタイププロパティで発見

というような順番で探っています。

しかし、次のようにするとプロトタイプ経由のプロパティを自分自身のプロパティとして利用することができます。

JAVASCRIPT:
  1. /*
  2.  ここでinstanceのプロトタイプチェーンをたどって
  3.  みつかったプロパティをOwnプロパティとして設定
  4. */
  5. instance.objprop=instance.objprop;
  6. instance.classprop=instance.classprop;
  7.  
  8. log(instance.hasOwnProperty("objprop"));
  9. // -->true
  10. log(instance.hasOwnProperty("classprop"));
  11. // -->true
  12. log(instance.hasOwnProperty("insprop"));
  13. // -->true

さて、プロトタイプチェーンの仕組みを踏まえて
次のようなプログラムを見てください。

JAVASCRIPT:
  1. /*
  2.     プロトタイプとしてメソッドを実装
  3. */
  4. Object.prototype.test=function(){alert(”hello”)};
  5. var t1={val:2};
  6. var t2={val:1};
  7.  
  8. // ===で同一のリファレンスか確認
  9. console.log(t1.test===t2.test);//true
  10.  
  11. /*
  12.     instanceのown propertyとして実装
  13. */
  14. var Class=function(){
  15. this.test=function(){alert(”hello!”);}
  16. }
  17. var c1=new Class();
  18. var c2=new Class();
  19.  
  20. //こちらは別のリファレンスになってしまう。
  21. console.log(c1.test===c2.test);//false

このように、オブジェクトのメソッドを定義する際に、
オウンプロパティとして実装する場合では、

2つのインスタンスの定義した関数のそれぞれのさすリファレンスがことなることがわかります。

リファレンスとはC言語でいえばポインタ。
つまりコンピュータのメモリ上の格納場所が異なるということです。

なので、

カプセル化した場合、コンストラクタ内でメソッドが定義されているので、new するたびfunctionが定義され、prototypeで定義するよりメモリを多くとられてしまうということはないのでしょうか?

冒頭のこのようなコメントを頂戴するわけです。
このコメントに対して、僕はあろうことか鼻くそほじるように
「トレードオフじゃないですか~。サーセンwwフヒヒ」(嘘おおげさ紛らわしい)と答えてしまいました。

やっぱりどのくらいコストがかかるのか確かめてみようと思い、実験を行いました。

実験内容

二つのjavascriptプログラムの実行時間とメモリ消費量を調べる。

JAVASCRIPT:
  1. /*
  2.     アンダーバー記法のクラス
  3. */
  4. //人間を作るときの関数(コンストラクタ)
  5. var Human=function(name,year){
  6.     this._year=year;
  7.     this.name=name;
  8.     this._checkEto();
  9. };
  10. //人間のDNA(設計図)を決める
  11. Human.prototype={
  12.     name:"",_year:1983,_eto:"亥",
  13.     //干支を生まれ年から計算する
  14.     _checkEto:function(){
  15.         this._eto=["子","丑","寅",
  16.             "卯","辰","巳",
  17.             "午","未","申",
  18.             "酉","戌","亥"][(this._year-4)%12];
  19.     },
  20.     //年齢を決める。
  21.     setYear:function(year){
  22.         this._year=year;
  23.         this._checkEto();
  24.     },
  25.     getYear:function(){return this._year;},
  26.     getEto:function(){return this._eto;}
  27. };

JAVASCRIPT:
  1. /*
  2.     クロージャによるカプセル化クラス
  3. */
  4. var Human=(function(){
  5.   var constructor=function(name,year){
  6.     var inner={
  7.       name:name,
  8.       eto:"",
  9.       year:year,
  10.       /*
  11.         private
  12.       */
  13.       checkEto:function(){
  14.         inner.eto=["子","丑","寅",
  15.           "卯","辰","巳",
  16.           "午","未","申",
  17.           "酉","戌","亥"][(inner.year-4)%12];
  18.       }
  19.     };
  20.     inner.checkEto();
  21.  
  22.     this.setName=function(name){
  23.       inner.name=name;
  24.     
  25.     };
  26.     this.setYear=function(year){
  27.       inner.year=year;
  28.       inner.checkEto();
  29.     };
  30.     this.getName=function(){
  31.       return inner.name;
  32.     };
  33.     this.getYear=function(){
  34.       return inner.year;
  35.     };
  36.     this.getEto=function(){
  37.       return inner.eto;
  38.     };
  39.   }
  40.   
  41.   return constructor;
  42. })();

この2つのプログラムの生成にかかるコストをこのような
プログラムをそれぞれに行い調査します。

JAVASCRIPT:
  1. for(var i=0;i<10000;i++){
  2. var x=new Human("DAICHI"+i,i);
  3. }

実行時間

# time js none.js
real    0m0.006s

# time js cap.js
real    0m0.007s

# time js underbar.js
real    0m0.007s

# time js c_cap_10000.js 
real    0m1.741s

# time js c_underbar_10000.js
real    0m0.867s

jsインタプリタにはspidermonkeyを使います。

none.jsはただjsインタプリタを起動するのにかかった時間をはかるために何も書いていないjsファイルをtouchして作っただけです。

読み込みにはそれぞれ0.001secしかかかっていません。
実行時間は10000個のインスタンス生成におよそ0.874秒のコストがかかっています。

ということは、カプセルかによって

87.4マイクロ秒

のコストが1つのインスタンス生成あたりにかかっているようです。

メモリ消費量について

次はメモリの消費量です。
これは昨日作ったfleafeedを使ってみます。
C言語でメモリ使用量をカウントする!

timeコマンドのオプション使えやって思った人は正しいんですが、
せっかくなので使ってみたくなっちゃったんだもん><

#fleafeed js none.js
< fleafeed >
-----------fleafeed results------------

--USING HEAP    :   48574 bytes

#fleafeed js cap.js
-----------fleafeed results------------

--USING HEAP    :   59641 bytes

# fleafeed js underbar.js
-----------fleafeed results------------

--USING HEAP    :   56019 bytes

# fleafeed js c_cap_10000.js
-----------fleafeed results------------

--USING HEAP    :12925508 bytes

# fleafeed js c_underbar_10000.js
-----------fleafeed results------------

--USING HEAP    : 1731685 bytes

とこんな感じ。

jsの実装はほとんどHEAPしか使わないので、HEAP領域のみをピックアップしてみました。

まず、読み込みにかかるコストの比較ですが

アンダーライン記法:11067 bytes
クロージャ記法	    :7745	bytes

あれ?なんかしらんけどクロージャ記法のほうが読み込みは少ないっぽいです。不思議。

次に生成1つあたりのメモリコストの違いですが
これはでかいですね。

これを計算してみると1つあたりのメモリコストは

1,110 bytes

となっているようです。およそ1kのメモリが余分に使われてしまうわけです。
これは確かにコスト高な感じはします。

富豪プログラミングをしたいひとにはオススメということで
どうでしょうか(泣)

]]>
http://yabooo.org/archives/70/feed 0
Javascriptでカプセル化を実現する! http://yabooo.org/archives/53 http://yabooo.org/archives/53#comments Mon, 28 Jan 2008 19:16:53 +0000 http://www.yabooo.org/archives/53

どうも、修士論文を出し終わって安心しきっているダメダメな院生のひろきのだいちです。

今回はJavascriptをつかって良いチラリズムと悪いチラリズムについてちょこっと話したいと思います。いったいなんのことやらさっぱりではあるんですが 、オブジェクト指向でいうところのカプセル化をプロトタイプベースであるところのJavascriptによって実現する方法やらなにやらを考察していきたいと思います。

目次:

  1. チラリズム
  2. カプセル化とは
  3. 人間クラスを作る
  4. スケスケクラスの問題点
  5. アンダーバー記法によるカプセル化
  6. アンダーバー記法の問題点
  7. クロージャとレキシカル変数
  8. クロージャで作る無名オブジェクト
  9. 無名オブジェクトを用いたカプセル化
  10. カプセル化ライブラリ

チラリズム

みなさんはチラリズムってスキですか?

少なくとも僕は大好きなんで、プログラムのことを話す前にチラリズムのことを話したいと思います。
想像してみてください。たとえば、とってもかわいい女の子が床に落ちたものを拾おうとして、
かがんだときに天使の羽的なブーラジャーがチラッとお目見えしたときのことを。
ステキやん。なんかステキやん。

でも、どうですか?

「ほら、ブラジャーやで~。見ぃ~へんかー」

とばかりに大胸筋矯正サポータを見せつけられることになったら。

イヤですよね。やめてほしいですよね。そうでもないっていうひとはとりあえず置いてきぼりにします。

いや、別にかまわないんだけども。

世の中、隠れているところに美徳があるんです。なにもかもあけっぴろげじゃやってられません。
それはプログラムの世界でもいっしょです。

カプセル化とは

カプセル化というのは・・・・IT用語辞典を引いてみます><

オブジェクト指向プログラミングが持つ特徴の一つ。データとそれを操作する手続きを一体化して「オブジェクト」 として定義し、オブジェクト内の細かい仕様や構造を外部から隠蔽すること。外部からは公開された手続きを利用することでしかデータを操作できないようにす ることで、個々のオブジェクトの独立性が高まる。カプセル化を進めることによりオブジェクト内部の仕様変更が外部に影響しなくなり、ソフトウェアの保守性や開発効率が高まり、プログラムの部分的な再利用が容易になる。

ほうほう。なるほど。なんのこっちゃと思わないでください。

要するに「中身の計算とかの細かいところを外部から隠して公開した方法だけ使ってください。というかそれ以外見ないで///」
ということです。

干支を自動的に調整するクラスを作る

それはさておき、プログラムの話です。とりあえJavascriptという言語でのオブジェクトの設計図であるところの クラスと言うやつをつくりましょう。ここでは、生年と干支をもったクラスをつくってみます。

//人間を作るときの関数(コンストラクタ)
var Human=function(name,year){
	this.year=year;
	this.name=name;
	this.checkEto();
};
//人間のDNA(設計図)を決める
Human.prototype={
	name:"",year:1983,eto:"亥",
	//干支を生まれ年から計算する
	checkEto:function(){
		this.eto=["子","丑","寅",
			"卯","辰","巳",
			"午","未","申",
			"酉","戌","亥"][(this.year-4)%12];
	},
	//年齢を決める。
	setYear:function(year){
		this.year=year;
		this.checkEto();
	}
};
var ore=new Human("hiroki",1983);
log(ore.name);
log(ore.eto);
ore.setYear(1999);log(ore.eto);

こんな感じです。
実行をクリックするとプログラムが実行されるので試してみて下さい。

クラスというのは人間で言うとDNAみたいなものです。
ここから本当の人間をつくるための処理のことをコンストラクタといいます。

このクラスは最初に名前と生年を入れたときに、自動的に干支を計算してくれるものです。
さらに、生年に間違いがあった場合にはsetYearというメソッドを使うことで、
その年にあった干支にメンバー変数を書き換えてくれます。

スケスケクラスの問題点

先ほどのクラスの問題点とはなんでしょうか?
それは、Javascriptのオブジェクトはすべてハッシュになっていて、アクセス権限をつけられないということです。
(注:最新のバージョンのJavascriptが動作する環境ならば、アクセサを設定することができるがすべてのブラウザで使用できるわけではない。)

アクセス権限をつけられないとはどういうことでしょうか?

このクラスを作ったひととは別の人がこのクラスを使用したときに
内部の構造まで簡単に書き換えられてしまい、思ったような動作をしない場合があるということです。

例を見てみましょう。

var ore=new Human("hiroki",1999);
log(ore.eto);
ore.year=1983;
log(ore.eto);
	

yearメンバーを書き換えたにもかかわらず、干支が反映されていません。
これはsetYearメソッドを使わずにyearにアクセスしてしまったためにおこりました。

このクラス設計では、「yearを使わずにsetYearを使ってください。」ということが
明言されてなかったためにこんなこともおきてしまいます。

それでは、この問題をどうやって解決したらよいのでしょうか。

アンダーバー記法によるカプセル化

昔から、プログラマたちはこの種の問題の解決策を考えるときに言語仕様などを変えることができないような
場合では「コーディング規約」を設定することで回避してきました。

その一例として、privateな変数は「_hogeFuga」のようにアンダーバーをプリフィックスにすることで、
簡単にアクセスできないようにしたり、名前が重複しないようにしたりという工夫がされてきました。

その場合のクラス例を見てみましょう。

//人間を作るときの関数(コンストラクタ)
var Human=function(name,year){
	this._year=year;
	this.name=name;
	this._checkEto();
};
//人間のDNA(設計図)を決める
Human.prototype={
	name:"",_year:1983,_eto:"亥",
	//干支を生まれ年から計算する
	_checkEto:function(){
		this._eto=["子","丑","寅",
			"卯","辰","巳",
			"午","未","申",
			"酉","戌","亥"][(this._year-4)%12];
	},
	//年齢を決める。
	setYear:function(year){
		this._year=year;
		this._checkEto();
	},
	getYear:function(){return this._year;},
	getEto:function(){return this._eto;}
};
//--TEST CASES--//
var ore=new Human("hiroki",1999);
log(ore.getEto());
ore.setYear(1983);
log(ore.getEto());
	

どうでしょうか?直接読み書きができるメンバー変数はnameのみで、
あとはsetやgetのついたメソッドを経由してアクセスしてくださいというお願いを
コーディングのなかのアンダーバーを使って書き込んでおきました。

これで、読んだ人はわかってくれるはず。と・・・

アンダーバー記法の問題点

ところがこの表記にも問題があります。
それは、
第一に中身にアクセスしようと思えばしてしまうということ。
第二に間違ったkeyでハッシュにアクセスしたときにエラーが出ずに
間違ったままのkeyにデータを代入してしまう可能性があるということ。
です。

例を見てみましょう。

//--TEST CASES--//
var ore=new Human("hiroki",1983);
log("亥になってほしい:"+ore.getEto());
ore.year=2008;
log("子になってほしい:"+ore.getEto());
	

上記の例ではsetYearでアクセスしなければならないところに
オブジェクトハッシュのyearに直接アクセスしてしまい、
本来の_yearを変更できないまま、間違ったyearというkeyに数字を代入してしまっています。

なので、当然干支も反映されずにそのままです。

クロージャ=そのときの変数を持ち続ける無名関数

そこでちょっと別のアプローチをして、この問題点を解決してみましょう。
Javascriptには、クロージャというものを作る機能があります。

クロージャは簡単に説明すると、
名前の無い関数をオブジェクトのようにして生成し、その関数を定義したタイミングで使える変数を
使えるままにして一時保存して、関数実行時にもアクセス可能にするという機能の代物です。

以下に例を示します。

function Test(argX){
	var x=argX;
	//このときはxにアクセスできる環境
	//それを引き継いだ無名関数を生成できる。
	return function(){x=x*x;return x;};

}

var func=Test(3);
log(func());
log(func());
log(func());
	

例では、関数のブロック内でしか利用できないxという変数をクロージャに記録しています。
このクロージャのなかからならば、xを利用することができるので、その値を関数が呼ばれるたびに
二乗しています。

クロージャで作る名無しのオブジェクト

このクロージャにはりついた変数をつかって、名前がなくなってしまったオブジェクトをつくり
そこにアクセスする方法を定義してみましょう。

//名無しのオブジェクトをつくる関数
function createObj(dat){
	var _anon=dat;
	//このときはまだ_objにアクセスできる
	return {
		get:function(){
			//_anonの中身をみるクロージャ
			return _anon;
		},
		set:function(dat){
			//_anonの中身を設定するクロージャ
			_anon=dat;
		}
	};
}
//ここでは_anonにアクセスすることができない。
//なのでクロージャ経由で、もう消えてしまったオブジェクトにアクセスする。
//--TEST CASES--//
var obj=createObj("hello!");
log(obj.get());
obj.set("こんにちは!");
log(obj.get());

//無理やり_anonにアクセスしようとしてもエラーが。
try{log(_anon);}catch(e){err(e)}

もし、ここでsetをするときに数字以外受け付けないようにすれば数字であることが保障される
変数を作ることができます。

名無しのオブジェクトを用いたカプセル化

それでは、先ほどもちいた名無しのオブジェクトを用いて、
アクセス制御を行ったクラスを見てみましょう。


var Human=(function(){
  var constructor=function(name,year){
    var inner={
      name:name,
      eto:"",
      year:year,
      /*
        private
      */
      checkEto:function(){
        inner.eto=["子","丑","寅",
          "卯","辰","巳",
          "午","未","申",
          "酉","戌","亥"][(inner.year-4)%12];
      }
    };
    inner.checkEto();

    this.setName=function(name){
      inner.name=name;
    
    };
    this.setYear=function(year){
      inner.year=year;
      inner.checkEto();
    };
    this.getName=function(){
      return inner.name;
    };
    this.getYear=function(){
      return inner.year;
    };
    this.getEto=function(){
      return inner.eto;
    };
  }
  
  return constructor;
})();
//--TEST CASES--//
var hachi=new Human("86世代",1986)
var ore=new Human("hiroki_daichi",1983);
log(hachi.getName()+":"+hachi.getEto());
log(ore.getName()+":"+ore.getEto());
//存在しないメソッドにアクセスしてみる。
try{ore.setEto("戌")}catch(e){err(e)}
	

コンストラクタから生成されるメンバが記録されているのはinnerという名前の
変数です。これはこの関数から抜けたあとにはアクセスすることができなくなります。

なのでcheckEtoのようなプライベート関数はinnerの中に書いておけば外から
アクセスされる心配がありません。

一方、アクセサやパブリック関数については、prototypeにクロージャとして代入しておけば、
innerにアクセスできる状態のままで外からでも利用可能な関数になります。

この記法を用いれば、
外部からすべてのメンバ変数へのアクセスはメソッド経由になるので、
存在しないメソッドにアクセスした際にかならずエラーが発生し、デバッグがしやすくなります。

カプセル化ライブラリ

おまけというか本ちゃんと言うか、
先ほどの手法を用いて、カプセル化を自動で行い、通常のクラスと同じように利用できるライブラリを
書いてみました。

詳しく説明することはしませんが、インスタンス生成時にinnerをメソッドにバインドすることで
名無しのオブジェクトをthisとして利用しています。


Function.prototype.bind=function(obj){
  var _method=this;
  return function(){
    return _method.apply(obj,arguments);
  };

};
Function.prototype.bindAsAccessor=function(attr,obj){
  var _method=this;
  return function(){
    var length=arguments.length;
    var tmp=[attr];
    for(var i=0;length-i>0;i++){
      tmp.push(arguments[i]);
    }
    return _method.apply(obj,tmp);
    
  };
};

var Class={
  create:function(obj){
    var Klass=function(){
      var inner={};
      
      for(var prp in obj){
      
        var elem=obj[prp];
        if(prp.match(/(attr|private|public)_(\w+)/gim)){
          var type=RegExp.$1;
          var prop=RegExp.$2;
          if(type=="attr"){
            inner[prop]=elem["preset"];
            if(elem.set){
              if(typeof elem.set=="function"){
                this["set_"+prop]=elem.set.bind(inner);
              }else{
                this["set_"+prop]=(function(attr,dat){
                  this[attr]=dat;
                }).bindAsAccessor(""+prop,inner);
              }
            }
            if(elem.get){
              if(typeof elem.get=="function"){
                this["get_"+prop]=elem.get.bind(inner);
              }else{
                this["get_"+prop]=(function(attr){
                  return this[attr];
                }).bindAsAccessor(""+prop,inner);
              }
            }
          }
          if(type=="private"){
            inner[prop]=elem.bind(inner);
          }
          if(type=="public"){
            this[prop]=elem.bind(inner);
          }
          
        }else{
          //識別詞の無いエントリ
          this[prp]=obj[prp];
        }
      }
      
      for(var prp in this){
        inner[prp]=this[prp];
      }
      Klass.__debug=obj;
      inner.initialize.apply(inner,arguments);
      
    }
    return Klass;
  }
};

var Human=Class.create({
  attr_year:{set:function(dat){
    this.year=dat;
    this.checkEto();
  },get:true,preset:0},
  attr_eto:{set:false,get:true,preset:"_"},
  attr_name:{set:true,get:true,preset:"_"},
  private_checkEto:function(){
    this.eto=["子","丑","寅",
      "卯","辰","巳",
      "午","未","申",
      "酉","戌","亥"][(this.year-4)%12];
  
  },
  private_initialize:function(name,year){
    this.name=name;
    this.year=year;
    this.checkEto();
  },
  public_dump:function(){
    log("---:dump object:---");
    for(var prp in this){
      if(typeof this[prp]!="function"){
        log(prp+":"+this[prp]);
      }
    }
  }
});
//--TEST CASES--//

var x=new Human("ひろきのだいち",1983);
var y=new Human("ひろきのだいち",1983);
x.dump();
y.dump();
x.set_year(1986);
x.set_name("86世代");
y.set_year(2008);
y.set_name("未来の主役");
x.dump();
y.dump();

それでは、君もいまからLet'sカプセル化ライフ!

]]>
http://yabooo.org/archives/53/feed 3