クラスフレームワーク
クラスフレームワークとは
インスタンスベースのクラスを手軽に利用するための枠組みです。Aelライブラリにおけるインスタンスベースとは関数型による動的なクラスをベースにオブジェクトを取り扱うことです。Java言語などの主流なオブジェクト指向言語のクラスとは毛色が違うので注意してください。
Java言語などのコンパイラ言語ではクラスは静的であり、基本的にプログラム実行時にクラス定義を変更することはできません。それとは対照的にJavaScriptのクラスはプログラム実行時にクラスを定義・変更します。また、当然クラスに定義しなくとも直接オブジェクトにメンバをプロパティとして持たせることもできます。これはJavaScriptがインタプリタ言語であり、そしてインスタンスベースであることが関係しています。二つの違いを感じてもらう為、以下に「作成したクラスからインスタンスを生成し、メソッドを呼ぶ」コードをJavaとJavaScriptでそれぞれ示します。
・Java
/* --- Klass.java --- */
// クラスを作成。
public class Klass{
public String value;
public Klass(String value){
this.value = value;
}
public String get(){
return this.value;
}
}
/* --- Main.java --- */
public class Main{
public static void main(String[] args){
// クラスからインスタンスを作成し、メソッドを呼ぶ。
Klass object = new Klass("aiueo");
System.out.println( object.get() ); // "aiueo"と表示される
}
}
・JavaScript
// クラスを作成。
var Klass = function(value){
this.value = value;
};
// クラスを作成後にメソッドを追加。
Klass.prototype.get = function(){
return this.value;
};
// クラスからインスタンスを作成し、メソッドを呼ぶ。
var object = new Klass("aiueo");
alert( object.get() ); // "aiueo"と表示される
// インスタンスを生成後にメソッドを追加することもできる。
Klass.prototype.get2 = function(){
return this.value + " kakikukeko";
};
alert( object.get2() ); // "aiueo kakikukeko"と表示される
// メンバはクラスに定義せずにオブジェクトに直接追加することもできる。
object.get3 = function(){
return this.value + " sasisuseso";
};
alert( object.get3() ); // "aiueo sasisuseso"と表示される
JavaScriptの関数型値は関数であり、クラスであり、さらにコンストラクタです。関数型値はnew演算子によってインスタンスを生成でき、その際にコンストラクタ(関数の中身)が実行されます。生成されたインスタンスからは関数型値のprototypeプロパティが持つプロパティを参照できます。このprototypeプロパティを複数の関数間で繋げることをプロトタイプチェーンと呼び、継承のメカニズムとして利用されています。Aelライブラリでも継承にプロトタイプチェーンを利用しています。
JavaScriptではクラスのメンバの種類にも違いがあります。Java言語などクラスベースのオブジェクト指向ではクラスは静的メンバとインスタンスメンバを持ちますが、JavaScriptではそれらに加えてプロトタイプメンバがあります。さらにAelライブラリではAelオブジェクトのメンバもクラスに定義する為、合計4種類のメンバを定義できます。これらのメンバはそれぞれ定義方法が違います。以下は4種類のメンバを定義し、参照するコードです。
// 静的メンバ
Array.staticProperty = "static";
alert( Array.staticProperty ); // "static"と表示される
// インスタンスメンバ
var array = new Array();
array.instanceProperty = "instance";
alert( array.instanceProperty ); // "instance"と表示される
// プロトタイプメンバ
Array.prototype.prototypeProperty = "prototype";
alert( array.prototypeProperty ); // "prototype"と表示される
// Aelオブジェクトメンバ
Ael.class_("Aiueo", {
"ael aelProperty" : "ael"
})
alert( Ael(new Ael.Aiueo()).aelProperty ); // "ael"と表示される
上記例では静的メンバとインスタンスメンバ、プロトタイプメンバはクラスフレームワークを使わずに定義しました。これをクラスフレームワークで定義すると次のようになります。
Ael.class_("Aiueo", {
"statc staticProperty" : "statc",
"instance instanceProperty" : "instance",
"prototype prototypeProperty" : "prototype",
"ael aelProperty" : "ael"
})
var aiueo = new Ael.Aiueo();
// 静的メンバ
alert( Ael.Aiueo.staticProperty ); // "static"と表示される
// インスタンスメンバ
alert( aiueo.instanceProperty ); // "instance"と表示される
// プロトタイプメンバ
alert( aiueo.prototypeProperty ); // "prototype"と表示される
// Aelオブジェクトメンバ
alert( Ael(aiueo).aelProperty ); // "ael"と表示される
クラスフレームワークを使うと少ない記述で定義できることがわかったと思います。クラスフレームワークとは上述したJavaScript特有のクラス定義の手続きを簡略化し、また各種メンバの定義方法の違いを吸収し、インスタンスベースのクラスを手軽に利用するための枠組みです。
使い方
クラスを作成する
クラスフレームワークでクラスを定義するにはAelクラス配下のパッケージ、またはクラスが持つclass_メソッドを使って行います。スーパークラスにしたいクラスのclass_メソッドに引数(第一引数にクラス名、第二引数にメンバを定義したオブジェクト)を渡して呼び出すと、スーパークラス配下にクラスが作成されます。作成されたクラスはデフォルトでスーパークラスを継承しています。
Ael.class_("Counter", {
"instance num" : 0,
"prototype initialize" : function(increment){
this.incrementValue = Ael.isNumber(increment) ? increment : 1;
},
"prototype increment" : function(){
this.num += this.incrementValue;
}
});
このクラスは次のように使うことができます。クラスフレームワークで定義したクラスはnew演算子によってインスタンス化します。またinitializeプロパティは特殊なプロパティでコンストラクタの役割を持ちます。従ってインスタンス化するとinitializeメソッドが呼び出されます。
var counter = new Ael.Counter(); // 0 counter.increment(); // 1 counter.increment(); // 2 alert(counter.num); // 「2」と表示される counter = new Ael.Counter(2); // 2 counter.increment(); // 3 counter.increment(); // 4 alert(counter.num); // 「4」と表示される
プロパティを追加する
クラスにはメンバとして4種類のプロパティを定義することができます。
| Aelプロパティ | Aelオブジェクトが持つプロパティ。 |
| インスタンスプロパティ | インスタンスが持つプロパティ。(Java言語のインスタンスフィールドと同じ) |
| プロトタイププロパティ | クラス単位で持つインスタンスプロパティ。プロトタイププロパティはインスタンスプロパティと同様の方法で参照する。同名のインスタンスプロパティが定義されている場合は、インスタンスプロパティが優先される。 |
| 静的プロパティ | クラスが持つプロパティ。(Java言語の静的フィールドと同じ) |
クラスにメンバを定義するにはクラスフレームワークが定めるメンバ定義仕様に従ったメンバ定義オブジェクトをclass_メソッドに渡す必要があり、メンバ定義オブジェクトは次の形式で作成します。
{
"(メンバ定義文字列)" : (値),
...
"(メンバ定義文字列)" : (値)
}
メンバ定義文字列はプロパティの種類と属性を指定するキーワードとプロパティ名から構成され、次の構文に従って記述することで簡単に様々なプロパティを定義することができます。また値にはプロパティに持たせたい任意の値を指定します。値が関数型の場合はメソッドとして定義されます。
メンバ定義文字列 ::= (keyword ' '+)? property_name keyword ::= (ael | 'static' | 'prototype' | instance)* property_name ::= [a-zA-Z0-9_]+ ael ::= 'void'? 'ael' instance ::= 'init'? 'instance'
では実際にCounterクラスにnumという名前のプロパティを4種類まとめて定義してみます。
Ael.class_("Counter", {
"ael instance prototype static num" : 0
});
var counter = new Ael.Counter();
alert( Ael(counter).num ); // aelプロパティ
alert( counter.num ); // instanceプロパティ(同名のインスタンスプロパティが優先されてプロトタイププロパティは参照されない)
alert( Ael.Counter.num ); // staticプロパティ
4種類のプロパティは個別に定義することもできます。
Ael.class_("Counter", {
"ael aelProperty" : 1,
"instance instanceProperty" : 2,
"prototype prototypeProperty" : 3,
"static staticProperty" : 4
});
var counter = new Ael.Counter();
alert( Ael(counter).aelProperty ); // 1
alert( counter.instanceProperty ); // 2
alert( counter.prototypeProperty ); // 3
alert( Ael.Counter.staticProperty ); // 4
次にメソッドを定義してみます。
Ael.class_("Counter", {
"ael aelMethod" : function(){
return 1;
},
"instance instanceMethod" : function(){
return 2;
},
"prototype prototypeMethod" : function(){
return 3;
},
"static staticMethod" : function(){
return 4;
}
});
var counter = new Ael.Counter();
alert( Ael(counter).aelMethod() ); // 1
alert( counter.instanceMethod() ); // 2
alert( counter.prototypeMethod() ); // 3
alert( Ael.Counter.staticMethod() ); // 4
メンバ定義文字列のキーワードには他に属性を指定するinitとvoidがあります。initキーワードはinstanceキーワードと合わせて指定する必要があり、また値は関数である必要があります。initキーワードを指定すると、インスタンス化時にコンストラクタと同様に関数を呼び出し、関数の戻り値をプロパティ値として設定します。
Ael.class_("Counter", {
"init instance num" : function(defaultValue){
return 100 + defaultValue;
}
});
var counter = new Ael.Counter(23);
alert( counter.num ); // 123
initキーワードは複雑な値を生成する場合などに使います。Aelはクラスのインスタンス化時にインスタンスプロパティに定義した値を複製(Ael#ael-clone()を使う)してインスタンスに設定します。これは複数のインスタンスの間で値が共有されないようにする為です。Ael#ael-clone()による複製は完全ではなく関数は複製することができません。従ってインスタンスプロパティに関数を含む値、または値としての関数を定義する場合はinitキーワードを使う必要があります。
最後にvoidキーワードはaelキーワードと合わせて指定する必要があり、また値は関数である必要があります。voidキーワードは関数型の値を持つAelプロパティ(Aelメソッドと呼ぶ)を定義した際に、暗黙的に作成される戻り値の違う三種類(※)のメソッドの内、「_XXXX」を定義しないようにすることができます。「_XXXX」は戻り値から新しいAelオブジェクトを生成して返すメソッドの為、戻り値がないメソッドには意味がありません。従ってinitキーワードを指定して無駄を無くす事ができます。
Ael.class_("CounterA", {
"instance num" : 0,
"ael increment" : function(){
this.val().num++;
}
});
Ael.class_("CounterB", {
"instance num" : 0,
"ael void increment" : function(){
this.val().num++;
}
});
var a = Ael(new Ael.CounterA());
alert( a._increment().val() ); // メソッドの戻り値(未定義値)が表示される
var b = Ael(new Ael.CounterB());
try{
b._increment();
}catch(e){
alert( e.message ); // voidキーワードにより_incrementメソッドは定義されていないのでエラーになる
}
既存クラスにプロパティを追加する
既に定義されたクラスにプロパティを追加するにはAel.lang.Class#ael-properties()を使います。
Ael.class_("Counter", {
"instance num" : 0,
"prototype initialize" : function(increment){
this.incrementValue = Ael.isNumber(increment) ? increment : 1;
}
});
Ael(Ael.Counter).properties({
"prototype increment" : function(){
this.num += this.incrementValue;
}
});
引数に渡す値はclass_メソッドの第二引数と同じです。
パッケージを作成する
パッケージとプロパティ名の衝突を避ける為のオブジェクトです。パッケージはAelクラス配下のパッケージ、またはクラスが持つpackage_メソッドを使って定義します。
Ael.package_("sample");
// パッケージは呼び出したpackage_メソッドを持つオブジェクトに作成されます。
alert(typeof Ael.sample); // object
パッケージは関連する機能を纏める為に使います。またパッケージを作成することでプロパティ名の衝突を回避することもできます。
// 同名のクラスを複数定義すると名前の衝突が起こり、最後に定義したクラスで上書きされる
Ael.class_("Counter", {
"instance num" : 0
});
Ael.class_("Counter", {
"instance num" : 1
});
alert(new Ael.Counter().num); // 1
// パッケージで分けることで名前の衝突を避けられる
Ael.package_("sample_a").class_("Counter", {
"instance num" : 0
});
Ael.package_("sample_b").class_("Counter", {
"instance num" : 1
});
alert(new Ael.sample_a.Counter().num); // 0
alert(new Ael.sample_b.Counter().num); // 1