どうも、修士論文を出し終わって安心しきっているダメダメな院生のひろきのだいちです。
今回はJavascriptをつかって良いチラリズムと悪いチラリズムについてちょこっと話したいと思います。いったいなんのことやらさっぱりではあるんですが 、オブジェクト指向でいうところのカプセル化をプロトタイプベースであるところのJavascriptによって実現する方法やらなにやらを考察していきたいと思います。
みなさんはチラリズムってスキですか?
少なくとも僕は大好きなんで、プログラムのことを話す前にチラリズムのことを話したいと思います。
想像してみてください。たとえば、とってもかわいい女の子が床に落ちたものを拾おうとして、
かがんだときに天使の羽的なブーラジャーがチラッとお目見えしたときのことを。
ステキやん。なんかステキやん。
でも、どうですか?
「ほら、ブラジャーやで~。見ぃ~へんかー」
とばかりに大胸筋矯正サポータを見せつけられることになったら。
イヤですよね。やめてほしいですよね。そうでもないっていうひとはとりあえず置いてきぼりにします。
いや、別にかまわないんだけども。
世の中、隠れているところに美徳があるんです。なにもかもあけっぴろげじゃやってられません。
それはプログラムの世界でもいっしょです。
カプセル化というのは・・・・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カプセル化ライフ!
« ネットワークケーブルをハサミでぶった切った – コンピュータって0と1だけなんだよね。 »
カプセル化した場合、コンストラクタ内でメソッドが定義されているので、new するたびfunctionが定義され、prototypeで定義するよりメモリを多くとられてしまうということはないのでしょうか?
var a=function(p){this.p=p;return this};
a.prototype.m=function(){alert(this.p)}
var aa=(new a(1)).m();
var aaa=(new a(2)).m();
↑この場合、メソッドmは同一のものを指すが、
var b=function(p){
this.m=function(){
alert(p);
}
}
var bb=(new b(1)).m();
var bbb=(new b(2)).m();
↑この場合、new の度mメソッドを定義するので、メモリ上別々のfunctionとして管理されるのでしょうか?
Comment: cyokodog – 30. January 2008 @ 12:42 am
cyokodogさん、そのとおりです。
トレードオフといえばそれまでですが、
さらに問題は、生成したあとのオブジェクトにprototypeベースで関数を追加した場合に
内部データにアクセスできないということも確かです。
この問題は専用の関数を用意すれば解決しますが、
複雑さは増します。
ECMAscriptのより高いバージョンでは、文法レベルでいろいろと提供されているので
解決するかもしれません。
Object.prototype.test=function(){alert(”hello”)};
var t1={val:2};
var t2={val:1};
console.log(t1.test===t2.test);//true
var Class=function(){
this.test=function(){alert(”hello!”);}
}
var c1=new Class();
var c2=new Class();
console.log(c1.test===c2.test);//false
Comment: hiroki_daichi – 03. February 2008 @ 5:21 pm
[...] Javascriptでカプセル化を実現する!の続編みたいなものです。 [...]
Pingback: 日本野望の会-Yabooo.org » Javascriptでカプセル化のコスト | – 07. February 2008 @ 9:29 pm