JavaScriptとprivateの見果てぬ夢 (JavaScript Advent Calendar 2011 オレ標準コース 6日目)

Posted by Hiraku on 2011-12-06

JavaScript Advent Calendar 2011(オレ標準コース)6日目です。他の方々のレベルが高すぎてビクビクしながら書いてます。

JavaScriptのオブジェクト指向は若干クセがあります。他のオブジェクト指向言語を使ってきた人にとって気になるのは、privateが作れないことでしょう。JSで教科書通り素直にクラス(もどき)を書くと、オブジェクトのプロパティは全てpublic、完全にオープンなものになってしまいます。

var Klass = function(){};
Klass.prototype = {
  methodA: function(){ ... },
  methodB: function(){ ... }
};
//----
var obj = new Klass();
obj.methodA(); //呼べる
obj.methodB(); //呼べる

言うまでもないですが、メンバー変数がいつでも好き勝手に書き換えられる状態ではカプセル化を簡単に破ることができ、オブジェクトの意味が薄れてしまいます。あと、console.dir()などで大きめのオブジェクトをdumpすると、どうでもいいユーティリティ関数などが表示されてしまい、デバッグに不便だったり…。

以前、私は「jsで無闇にprivateを実現しようとか考えない方が良い」と書いたことがあります。結局、教科書的な文法が一番簡単かつ綺麗なコードになります。しかしそれでも追い求めるのが夢でございます。

これまで多くの人がprivate問題をどうにかしようとしてきました。今更ですがまとめてみることにします。

A. 紳士協定

var Klass = function(){};
Klass.prototype = {
//public
  methodA: function(){ ... },
//private
  _methodB: function(){ ... }
};
//--------------------

var obj = new Klass();
obj.methodA(); //呼べる
obj._methodB(); //呼べるけど呼ぶなよ!!

諦めてpublicだけで作るやり方。"_"(アンダースコア)から始まる要素はprotected/privateなものなので外から呼ばないで!というオレオレルールを作っただけです。解決になってませんが、結構よく使われています。

■メリット

  • 実行速度・オブジェクトの生成速度共に速い。(jsの言語機能そのままだもんね)
  • prototypeをきちんと使ったコードなので省メモリ。
  • 実質publicなので継承できる。(protected)

■デメリット

  • 問題の解決になってない。

B. 全部コンストラクタで定義する

var Klass = function(){
  var privateA = function(){ ... };

  this.publicA = function(){
    //privateを呼ぶとき
    privateA.call(this, arg1, arg2, ...);
    //publicを呼ぶとき
    this.publicB();
  };
  this.publicB = function(){ ... };
};
//---------------------------------
var obj = new Klass();
obj.publicA(); //呼べる
//privateA(); //呼べない

クロージャで自然とカプセル化されるので問題は解決。privateを呼ぶ際にcall(this)を使わないとprivateの中からpublicが呼べなくなります。

■メリット

  • privateが実現できている。
  • private, publicともに同じインターフェースでお互いの値にアクセスできる。

■デメリット

  • prototypeを使っていないのでメモリ消費が激しい。大量にオブジェクトを生成する場合に不向き。
  • privateなメンバは継承できない。いわゆるprotectedは再現できない。
  • うっかりコンストラクタ中に副作用のあるコード(画面上に何か出力するとか)が紛れてしまうと継承しにくくなる。

行儀が悪いのでこういうコードは個人的にあまり好きじゃないです。書き方がシンプルなので書き捨てプログラムには向くかも。

C. privileged

Douglas Crockfordのprivateに関する記事によるもの(2001年のだけど)。

コンストラクタを使いつつprototypeを一部併用する書き方で、要はAとBの折衷案になります。

var Klass = function(){
  //private
  var privateA = function(){};
  
  //privileged
  this.privilegedA = function(){
    //privateにアクセスできる
    privateA.call(this, arg1, ..);
    //privilegedにアクセスできる
    this.privilegedB();
    //publicにアクセスできる
    this.publicA();
  };
  //...
};
Klass.prototype = {
  publicA: function(){
    //publicの中からprivateは呼べない
    //privilegedは呼べる
    this.privilegedB();
  }
};

//---------------------------
var obj = new Klass();
obj.publicA(); //呼べる
obj.privilegedA(); //呼べる
//privateA(); //呼べない

■メリット

  • privateが実現できている
  • prototypeを併用するのでメモリの消費もそこそこに抑えられる

■デメリット

  • パッと見でpublicの存在意義がわかりにくい。privateにアクセス出来ないpublicって何なの。
  • privilegedという新しい概念が登場する
  • 全部privilegedにしてしまう誘惑に駆られる
  • protected実現不可

個人的には、privilegedの概念が難しいと思いました。3つもあったらどのメソッドがどの権限だったかすぐ混乱しそうです。。「publicにせずにprivilegedで全部書けばいいだろ!」と勘違いする人がでてきそうだし。このスタイルを使いこなせる自信が無いです、マスター。

D. prototypeをクロージャに閉じ込める

var Klass = function(){};
(function(){
  var privateA = function(){};

  Klass.prototype = {
    publicA: function(){
      //privateを呼ぶ
      privateA.call(this, arg1, arg2, ...);
      //publicを呼ぶ
      this.publicB();
    },
    // ...
  };
})();

//------------------------
var obj = new Klass();
obj.publicA(); //呼べる
//privateA(); //呼べない

即時関数をひとつ用意してBを変形しました。短い割にメソッドがカプセル化できています。

■メリット

  • メソッドはprivateを実現しつつ、prototype併用によりメモリの消費も少ない
  • private、publicともにお互いにアクセス可能

■デメリット

  • メソッドはカプセル化できたけど、プロパティがカプセル化できていない。

E. private用のオブジェクトを分割する

最近はこんな書き方を試したことも。

/**
 * class Klass
 */
var Klass = function(){
  this._ = new Klass_;
};
// public
Klass.prototype = {
  init: function(name){
    //privateはthis._. XXXX . call(this, xxx)で呼ぶ
    this._.setName.call(this, name);
    //private変数はthis._.XXXを直接引っ張ればOK
    this._.moge = 'foo';
    //publicは素直にthis.で呼び出す
    this.sayHello();
  },
  sayHello: function(){
    console.log("hello, "+this._.name);
  }
};

// private
var Klass_ = function(){};
Klass_.prototype = {
  setName: function(name) {
    //
    this.name = name;
  },
  methodB: function(){
    console.log("B");
  }
};

//--------------------------------
var obj = new Klass();
obj.init("Taro");

//obj.setName(); //呼べない

なんだかごちゃごちゃしていますが、要はprivate用public用でクラスもどきを2個作って、2個のオブジェクトを合わせて使っているわけです。

■メリット

  • prototypeをきちんと使っているので省メモリ。
  • publicをごまかしただけなので、publicもprivateも継承できる。(protectedと書くのが正しい?)
  • console.dir(obj)でprivateメンバが延々と表示されたりしない。漏れ出すのは"_"というメンバだけ。

■デメリット

  • やや複雑。
  • privateの呼び出しコードが長い。this._.privateA.call(this, arg1,arg2)... 長い。
  • "_"を経由すればprivateっぽい要素にもアクセスできてしまう。結局は問題の解決になってない。

F. DとEの折衷案

メンバーだけ押し込めたら綺麗なんでは?とか。

var Klass = function(){
  this._ = {};
};
(function(){
  var privateA = function(){
  };

  Klass.prototype = {
    publicA: function(){
      //publicなものはthis.でアクセスする
      this.publicB();
      this.publicMember = "a";
      //privateメソッドはcallで
      privateA.call(this, ...);
      //privateメンバはthis._に押し込める
      this._.privateMember = "a";
    },
    //...
  };
  
})();

メリット・デメリットも折衷といったところですね。


ひとまずまとめ

そろそろ飽きてきたのでやめます。今のところは適宜書き方を決めて、やりたいようにやるしかないですね。(投げやり)

JavaScriptでカプセル化したクラスを実現しづらいのは、オブジェクトがもっぱら「再利用のための単位」として位置づけられていて、カプセル化の機能であるクロージャとベクトルが違うせいかもしれません。

keyword: javascript AdventCalendar

JavaScriptの最新記事