No.10 クラスシステムの理解
No.5 Senchaにおけるデザインパターン でも触れたように、Sencha のフレームワークは、 prototype 型のオブジェクト指向言語である JavaScript に、伝統的な「クラス」型のオブジェクト指向開発の優れた部分を取り入れたフレームワークとなっています。
今回は、その Sencha フレームワークのクラスシステムについて理解を深めようと思います。
Ext.defineメソッド
Sencha フレームワークでクラスを定義するには、 Ext.define メソッドを使います。
Ext.define メソッドは新しいクラスを定義したり、既存のクラスを継承する子クラスを作成する場合に使用します。
単純なクラスを定義してみましょう。
1 2 3 4 5 6 | Ext.define('MyApp.BaseClass',{ baseName: 'BaseName', newMethod: function(){ console.log('newMethod was called.'); } }); |
Ext.define の第1引数には、新しく定義するクラスのクラス名を文字列で指定します。 第2引数には、そのクラスの構造を定義するオブジェクトを渡します。 通常は、このように定義オブジェクトはオブジェクトリテラルで記述します。 ここでは、BaseClass という新しいクラスが定義されます。 このクラスは、全く新しいクラスですが、実は、Ext.Base という Sencha フレームワークの基底クラスを継承しています。 そしてここでは、クラスに baseName というプロパティと newMethod というメソッドを定義しています。
Sencha でのクラス継承
Sencha フレームワークにおいてクラスを継承する場合には、クラス定義の中の extend 指定で継承元のクラスを指定します。 extend 指定は、クラスの定義時と同じようにクラス名を文字列で指定します。
1 2 3 4 5 6 | Ext.define('MyApp.SubClass', { extend: 'MyApp.BaseClass', newMethod: function(){ console.log('SubClass.newMethod was called.'); } }); |
この例では、先ほど定義した BaseClass を継承して、SubClass を定義しています。 親クラスにある全てのメソッド、イベント、プロパティは、継承した子クラスでも利用可能です。 ですから BaseClass にある baseName というプロパティを参照する事ができます。 またクラスにも、newMethod が定義されていますので、SubClass においては newMethod がオーバーライドされます。 オーバーライドとは、子クラスと親クラスに同名のメソッドが存在するとき、親クラスのメソッドの代わりに、子クラスのメソッドが呼び出されることを言います。 親クラスの同じメソッドを呼び出したい場合は、オーバーライドした子クラスのメソッドの中で、 callParent メソッドを呼び出します。
1 2 3 4 5 6 7 | Ext.define('MyApp.SubClass', { extend: 'MyApp.BaseClass', newMethod: function(){ this.callParent(arguments); console.log('SubClass.newMethod was called.'); } }); |
このようにすると、子クラスの newMethod が呼び出されると、その中で親クラスの newMethod を呼び出せます。 Sencha フレームワークにおいてUI部品として利用されるクラス (コンポーネント) は Ext.Component クラス (あるいはそのサブクラス) を継承します。 また、他のコンポーネントを内包するクラスは Ext.Container クラスを継承します。Ext.Container クラスも Ext.Component クラスを継承しています。
インスタンス化
定義したクラスは通常それをインスタンス化してオブジェクトを生成します。 Sencha フレームワークでは、インスタンス化する際には次のように Ext.create メソッドを使います。
1 | var obj = Ext.create('MyApp.SubClass'); |
次のように第2引数にクラスを初期化するためのコンフィグオブジェクトを指定することもできます。
1 | var obj = Ext.create('MyApp.SubClass', {...}); |
Ext.define でクラス定義がされると、コンストラクター関数が定義されますので、 Sencha フレームワークにおいても、通常の JavaScript のように new 演算子を使ってクラスのインスタンスを作ることは可能ですが、通常は、Ext.create を使うことが推奨されます。
1 2 3 4 5 | // new演算子による方法 var obj1 = new MyApp.SubClass({...}); // Ext.create による方法 var obj2 = Ext.create('MyApp.SubClass',{...}); |
前者では、クラス定義がされる前にこの式が実行されると、ランタイムエラーになってしまいますが、後者では次に述べる動的ローディングによって必要なクラスを動的に読み込むことが可能になります。
クラスの動的ローディング
Sencha フレームワークには、動的ローディング機能が備わっています。 Ext.Loader で動的ローディングを設定することにより、index.html に JavaScript ファイルを全て記述することなく、必要なクラスが格納されたファイルが適切な順番で動的にローディングされるようになります。
動的ローディングの設定
1 2 3 4 5 6 7 8 | Ext.Loader.setConfig({ enabled: true, // 動的ローディングを有効にする paths: { 'MyApp': 'app' // クラスの名前空間とファイルが格納されているパスを指定(複数可能) } }); Ext.require('MyApp.some.Class'); // クラスをロード |
Ext.require メソッドを実行すると、ファイルからクラスをロードします。Ext.create と違って、それをインスタンス化はしません。単にクラス定義をロードするだけです。 Ext.Loader が適切に設定されていれば、Ext.create でクラスのインスタンスを作成した時に、まだクラス定義がされていない場合はクラス定義をロードしてからインスタンス化してくれます。 さらに、ロードしたクラスが他のクラスを継承しているなら、必要な親クラスもロードしてからクラス定義します。
クラスファイルに関する各種決まり事
クラス定義が記述されているファイル読み込みは、クラス名を元に行われているのですが、これを実現するためにSencha フレームワークではいくつかの決まりが定められています。 まず、クラス名とファイル名は1対1で対応しています。
- 1ファイルには1クラスだけを定義します。
- ドットで繋がれた名前空間はディレクトリ名に、クラス名はファイル名となります。 上記の例では、paths 指定で MyApp は app ディレクトリと定義されているので、 “MyApp.some.Class” は “app/some/Class.js” に配置することになります。
そしてクラス名の付け方には名付け規則があります。
- クラス名の頭文字は大文字。複数単語からなるクラス名はアンダースコアなどは使わずアッパーキャメルケースとします。 例えば、Number_Convert ではなく NumberConvert にします。
- トップレベルの名前空間 (例:MyApp) についてもアッパーキャメルケースとします。その途中の名前空間 (パッケージ名) は全て小文字とします。
- 頭文字を使った略語等であっても、クラス名に使う場合はアッパーキャメルケースとします。HTTP ではなくHttp とします。
- クラスのメソッド名やプロパティ名は、ロワーキャメルケースを使います。getFirstName というようにします。
クラスのその他の機能
ここまでが、Sencha フレームワークにおけるクラスの基本ですが、クラスシステムの中にいろいろと便利な機能が備わっています。次にそれらを紹介しましょう。
config による setter/getter の自動生成
オブジェクト指向の世界では、内部的なプロパティを private として、そのプロパティにアクセスするために、getter と setter と呼ばれるメソッドを定義することがよく行われます。 Sencha フレームワークでは、それらの getter/setter を自動生成する機能があります。
config 指定の中に、キー/値ペアを記述すると、 そのプロパティに対する、setter/getter メソッドが自動的に生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Ext.define('MyApp.Person',{ config: { gender: 'female', age: 30, weight: 50, height: 160 }, constructor: function(cfg){ this.initConfig(cfg); ... } ... }); var male = Ext.create('MyApp.Person'); male.setGender('male'); // 自動生成されたsetterメソッド male.setAge(50); // 自動生成されたsetterメソッド console.log(male.getWeight()); // 自動生成されたgetterメソッド console.log(male.getHeight()); // 自動生成されたgetterメソッド |
config 指定の中に4つのキーがセットされています。 これらの一つひとつに getter/setter が生成されます。 上記のクラスでは、getGender/setGender、getAge/setAge、getWeight/setWeight、getHeight/setHeight というメソッドが使えるようになるわけです。
ここで一つだけ注意が必要です。setter/getterを自動生成させるには、constructor の中で initConfig メソッドの呼び出さなければならないということです。 ただし、Ext で始まる Sencha フレームワークのクラスを継承する場合には、その必要はありません。なぜなら上位クラスの constructor で initConfig メソッドが呼び出されるからです。
statics による静的プロパティ/メソッドの定義
statics 指定の中にキーを定義すると、その中のプロパティやメソッドは、静的プロパティ/静的メソッドになります。 つまりクラスをインスタンス化しなくても使えるメソッドでありプロパティになります。
1 2 3 4 5 6 7 8 9 10 11 | Ext.define('MyApp.Person',{ statics: { // 静的プロパティとメソッドの定義 star: 'Earth', getNumberOfEyes: function(){ return 2; } } ... }); MyApp.Person.getNumberOfEyes() // クラスの静的メソッドの呼び出し |
mixins によるクラスのミックスイン
Sencha フレームワークにはミックスインという機能があります。 これはクラスを継承するのではなく、あるクラスに他のクラスの持つ機能を追加する(混ぜ込む:ミックスイン)ことができる機能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Mixinするクラスの定義 Ext.define('MyApp.Mother', { config: { hairColor: 'blond', eyeColor: 'blue' } }); // Mixinする別のクラスの定義 Ext.define('MyApp.Father', { config: { skinColor: 'brown', eyeColor: 'brown' } }); Ext.define('MyApp.Child',{ mixins: { // Mixinの取り込み mother: 'MyApp.Mother', father: 'MyApp.Father' }, constructor: function(cfg){ this.initConfig(cfg); ... } ... }); var kid = Ext.create('MyApp.Child'); kid.getHairColor(); // Mixinクラスのメソッドを自分のメソッドとして呼び出し kid.mixins.father.getEyeColor(); // Mixinクラスのメソッドを指定して呼び出し |
三つ目のクラス (MyApp.Child) には mixins 指定があります。ここで、前の二つのクラスをミックスインしています。 そうすると、MyApp.Child にはミックスインされたクラスの特性をあわせ持つことになります。 前の例では、MyApp.Mother クラスのメソッドも MyApp.Father クラスのメソッドも自分のクラスのメソッドとして使えます。
また、ミックスインに指定したキー名のプロパティの中にあるメソッドからも使えます。 この例では eyeColor というメソッドはミックスインした二つのクラスの双方にありますね。 最後の行では、どちらの eyeColor メソッドを呼び出すのかはっきりさせるために、father.getEyeColor() と呼び出しています。
コンフィグオプションの隠蔽
Ext.create メソッドにはクラスを初期化するためのコンフィグオプションを渡すことができます。 しかしいつも同じようなコンフィグオプションを指定して、クラスを生成するのはちょっと無駄な感じがします。 そういったときには、クラスの継承を利用して、クラス定義の際にコンフィグを指定します。 そうすれば、そのクラスをインスタンス化するときには、最小限のコンフィグオプションの指定するだけで済ませることができます。 結果としてアプリケーションコードを短くするだけでなく、保守性も高めることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 再利用可能なアプリケーション用のクラスを作成: Ext.define('MyApp.view.MyGrid', extend: 'Ext.grid.Panel', xtype: 'mygrid', // xtypeを設定 title: 'Product Inventory', store: new Ext.data.Store({ proxy: { reader: // readerコンフィグオプション }, data: // ロードするデータ }), columns: [ // columnコンフィグオプション ] }); |
これは、Sencha Ext Js で Grid コンポーネントを使う時のコードです。 最初の Ext.define ブロックでは、Ext.gird.Panel (グリッドパネル) を継承した、MyApp.view.MyGrid というクラスを定義しています。 このように定義したクラスを使う時には、
1 | var grid = Ext.create('MyApp.view.MyGrid'); |
と簡単にインスタンス化することもできますし、コンポーネントに特有の xtype を指定することで、より簡単にそのコンポーネントを使うことができます。
1 2 3 4 5 6 7 | // アプリケーションコードが読みやすく短く Ext.create('MyApp.view.Viewport', { layout: 'border', items:[{ xtype: 'mygrid' }] }); |
xtype での遅延インスタンス化については No.6 コンポーネントモデル で説明しましたが、上の例ではそれを使って Viewport の中の子アイテムとして、さきほどのクラスをxtypeで指定してセットしています。 これだけで、MyApp.view.MyGrid コンポーネントが使えるようになるのです。 とても便利でしょう?
プラグイン
コンポーネント (Ext.Componentのサブクラス) の場合には、プラグインを利用することによりクラス間の継承関係に影響を与えることなく、既存のコンポーネントの機能を拡張することができます。 全てのコンポーネントクラスに対して一つのプラグインを適用できれば、とても役に立ちます。 プラグインを作る際の唯一の条件は、init メソッドを用意することです。 init メソッドにはプラグインが埋め込まれる側のオブジェクトが引数として渡されます。
次の例は、タイトルバーに★を追加するプラグインです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Ext.define('StarAppendPlugin', { alias: 'plugin.starappend', init: function(cmp) { var f = function(cmp, newTitle, oldtitle) { var star = '★'; cmp.un('titlechange', f); cmp.setTitle(star + newTitle); cmp.on('titlechange', f); }; cmp.on('titlechange', f); } }); |
init メソッドの内部にある f 関数の中でいろいろやっていますが、要は、タイトルが変更されたときに、タイトルの前に★マークをつけてセットする機能を、渡されたコンポーネントに対して追加しています。 init に渡されてきたコンポーネントをどう扱うのかはそのプラグイン次第です。 alias コンフィグに ‘plugin.foo’ という形式でプラグインの別名を定義しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | var win = Ext.create('Ext.window.Window', { title: '最初のタイトル', width: 500, height: 300, plugins: ['starappend'], buttons: [{ text: 'タイトル変更', handler: function() { win.setTitle('変更されたタイトル'); } }] }); win.show(); |
そしてプラグインを使うときにはコンフィグオプションの plugins にプラグインのエイリアス(別名)をセットします。プラグインのクラス定義で alias: ‘plugin.foo’ と指定した場合には foo をセットします。
Sencha フレームワークには、既に用意されている便利なプラグインも多くあります。
今回はクラスシステムについて学びました、次回はテンプレートとデータビューについて学びます。