読者です 読者をやめる 読者になる 読者になる

is Neet

ネトゲしながら暮したい

javascriptでのオブジェクト指向プログラミングをサポートするライブラリ「pia.js」を作りはじめました

2012-08-05

大げさに言ったけど要はjavascriptでのクラス宣言を簡単に行えるライブラリです。
まだ試験的に作っただけなので実用性は低いです。

https://github.com/soplana/pia

ある程度完成したら取り敢えずは自分のプロジェクトとかで使ってみようと考えています。
なんかjavascriptのpublic/privateを考えていたら面白かったので、まとめておきます。



なにがしたい(できる)ライブラリなのか

個人的にjavascriptは好きな言語なのですが、書いていて気になるのが数百行単位になってくると一気に可読性が低くなる点です。
javascriptである程度の規模のオブジェクトを書いていると、どういったインターフェイスを用意しているオブジェクトなのかが良く分からなくなってきます。
単純に他のオブジェクト指向言語同様、public/privateのアクセス宣言やclassメソッド/instanceメソッドの宣言が出来ていれば可読性は一気に上がるのではないかと予測しました。

なのでpia.jsは、他のオブジェクト指向言語で普通に出来ている、public/privateの宣言やclassメソッドの宣言をサポートします。



取り敢えず「pia.js」の使い方

var Library = $class({  // pia.makeClass

  // constructor
  initialize : function(name){
    this.name = name; // private instance property
  },

  // public instance methods
  public : {
    getName : function(){
      return this.prefix() + this.makeName();
    },
    
    setName : function(name){
      return this.name = name;
    },
    
    prefix : function(){ 
      return "Javascript Library: ";
    }
  },
  
  // private instance methods
  private : {
    makeName : function(){
      return this.name.toUpperCase();
    }
  },

  // class method
  self : {
    // public class method
    public : {
      libraryName : function(){
        return "pia" + this.extension();
      }
    },
    // private class method
    private : {
      extension : function(){
        return ".js";
      }
    },
  }
});


console.log( Library.libraryName() ); // pia.js

var library = Library.new("pia.js");

console.log( library.name );          // undefined
console.log( library.getName() );     // Javascript Library: PIA.JS 

library.setName("jquery");

console.log( library.getName() );     // Javascript Library: JQUERY 
console.log( library.makeName() );    // library has no method 'makeName'

$class or pia.makeClassメソッドにオブジェクトリテラルを渡してClass宣言をします。
initializeというfunctionはインスタンス生成時に動くコンストラクタの役割を果たすfunctionです。
このClassのタネとなるオブジェクトリテラルには、public/privateというプロパティを定義することができます。
pia.jsでは、このpublic/privateというプロパティに、functionを定義したオブジェクトリテラルを渡す事で、インスタンスメソッドを定義することが出来ます。

インスタンス変数を宣言したい場合は、initializeまたはインスタンスメソッドの中でthisに対して追加します。
現状のpia.jsでは、ここで宣言したインスタンス変数はprivateなプロパティとして扱われるので、外部から扱いたい場合はgetter/setterを定義する必要があります。

クラスメソッドを定義したい場合は、selfというプロパティにネストした形でインスタンスメソッド宣言と同様にpublic/privateプロパティを持ったオブジェクトリテラルを渡します。



javascriptにおけるpublic/private

これは厄介な問題です。
この問題については下記のブログが非常に参考になります。
ホントに良い記事です。


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


また、コチラのリンクも本当に素晴らしく纏められています。


Private Members in JavaScript


privilegedという新しい概念(僕にとっては)もありましたが、これでオブジェクトを組み立てていると大変混乱してしまい可読性はさらに下がるなと感じました。
これはまぁ僕のjavascript力が低いせいかもしれません。


個人的に一番いいなと思う実装は、public/privateメソッドをクロージャのスコープに閉じ込める実装です。
上記ブログでいうと、「D. prototypeをクロージャに閉じ込める」の実装です。
勝手に引用して申し訳ございません。
しかも僕個人の意見なので、この下りはどうでもよかったです。



pia.jsで何が行われているか

一旦public/privateから話は逸れます。
まずはざっくり全体の流れを。


pia.jsでのclass宣言、インスタンス生成時の処理の流れを書きます。
ざっくり流れを説明すると、まず最初にpia.makeClassメソッドが呼ばれると、makeClassメソッドに渡されたオブジェクトリテラルを元にnewメソッドを持ったオブジェクトが生成されます。
これがclass宣言にあたります。
さらにそのオブジェクトに対しnewメソッドを呼ぶと、instanceオブジェクトが生成される流れになっています。
makeClassから生成されるオブジェクトと、newメソッドから生成されるinstanceオブジェクトの生成は、pia.js内部で行っている処理としてはほぼ同様の内容になっています。



pia.jsでどうやってpublic/privateを実装したか

具体的に.new()が呼ばれた時の流れを書きます。

  • pia.InstanceConstruction.prototype.newが呼ばれます。
  • その中で内部的に参照する為のPiaBaseObject Classが宣言されます。
  • makeClassに渡されたオブジェクトリテラルを元にpublic/privateメソッドがPiaBaseObjectのprototypeに追加されます。(この時点ではpublic/privateの区別はありません)
  • makeClassに渡されたオブジェクトリテラルにinitializeが定義されていた場合は実行します。
  • initializeを実行する際のthisはPiaBaseObjectのインスタンスになります。(applyを使って)
  • 次にPiaBaseObjectを使って実際に.new()の返り値となるオブジェクト、PiaObject Classを作成します。


以下にPiaObject生成の話を書きますが、興味がある方はソースを読んだほうが早いかもしれません。
pia.jsのpia.InstanceConstruction.prototype.convertがそれに当たります。


さて、ここからが少し複雑なのですが、PiaObject自体はprivateメソッドを持ちません。
evalにより上書きしたpublicメソッドだけをもち、そのpublicメソッドにはapplyを使い(隠蔽されていますが)PiaBaseObjectのインスタンスをthisとして呼び出すようにします。
つまり、厳密には違いますがPiaObjectはPiaBaseObjectのラップクラスのような形で機能します。
なのでpublicとして定義されたfunctionはPiaObjectのprototypeにevalで手を加えて追加され、privateとして定義されたfunctionはPiaObjectのfunction内部からthis(applyによりPiaBaseObjectのインスタンスを指す)でのみアクセス出来る様になっています。



継承について

pia.jsはclassの継承もサポートしています。
以下サンプルコード

var User = $class({
  initialize : function(){
    this.name = "guest user";
  },

  public : {
    signIn : function(prefix){
      return (prefix || "welcome") + " " + this.name;
    }
  },

  private : {
    message : function(isAdmin){
      return isAdmin ? "admin user" : "guest user";
    }
  }
});

var AdminUser = $class({
  initialize : function(name, pass){
    this.name = name;
    this.pass = pass;
  },

  public : {
    signIn : function(){
      if(this.pass === 1234)
        return $super("hello");
      else
        return "could not sign in";
    },

    isAdmin : function(){
      return this.message(true);
    }
  }
}).extend(User);

var GuestUser = $class({
  public : {
    isAdmin : function(){
      return this.message(false);
    }
  }
}).extend(User);


var adminUser = AdminUser.new("soplana", 1234);
console.log( adminUser.isAdmin() );                    // admin user 
console.log( adminUser.signIn()  );                    // hello soplana 
console.log( AdminUser.new("soplana", 123).signIn() ); // could not sign in 

var guestUser = GuestUser.new();
console.log( guestUser.isAdmin() );                    // guest user 
console.log( guestUser.signIn()  );                    // welcome guest user

javascriptの命名規則的には__super__()なのでしょうけど、タイピングが少なくて済む_super_()を採用しました。
_super_()から$super()に変更しました。

内部では悪い意味でグロい事してます…。あまり褒められた実装ではない気が…。
上記の例でいうとUser Classを継承したAdminUser Classをさらに他のClassで継承すると言った事がまだ未対応ですが。
対応しました。


使いどころと今後

内部的に3~4個のオブジェクトを生成している上に、PiaObjectがクロージャで生成されているので余計な参照が残っているのでメモリ効率は悪いと思います。
ガンガンオブジェクトを生成しまくる処理には向いていないです。
ここはもっと効率のいい方法があれば随時効率化していければなと考えています。


次に継承周りを実装すれば、取り敢えず自分のプロジェクトで使ってみて改善して大丈夫そうなら正式にリリースしようかなと考えています。
言語としては、やや嫌われ者感のあるjavascriptですがpia.jsにより心地良いオブジェクト指向プログラミングが出来るようになればいいなと思います。


言い訳臭くなりますが、僕自身javascriptの事を深く理解しているとは言えない状態なので、pia.jsを作っていて勉強になることが多いです。
今回の実装をしていて、心底javascriptが面白くて仕方なかったのでこれからより理解を深めて改善していければいいなと思っています。
あと、どんどんライブラリ作っていきたいな、とも。