Ext JS 4 クラスシステム

SenchaのAPIドキュメンテーションにある Ext JS 4 Class System を読んでみました。


Ext JSは,その歴史の中で初めて,新しいクラスシステムへと大幅にリファクタリングされました。新しいアーキテクチャは,Ext JS 4に書かれているほとんどのクラスの後ろ盾となっていますので,コーディングを始める前に,それをよく理解することが重要です。

このマニュアルはExt JS 4を使って新しく作りたい,または既存のクラスを拡張したいという開発者を対象とします。このマニュアルは3つのメインセクションに分かれています。

  • Ⅰ.概要 – 堅牢なクラスシステムの必要性について説明します。
  • Ⅱ.ハンズオン – 詳細なステップバイステップのコード例を提供します
  • Ⅲ.エラーハンドリングとデバッギング – 例外を扱うために役に立つチップスとトリックを提供します
  • Ⅲ.中身を見る – 技術的コンセプトと実装について論じます。

I. 概要

Ext JS4には300以上のクラスがあります。 世界中には20万人以上の様々なプログラミングバックグラウンドを持つ開発者の巨大な共同体があります。このような規模の組織で,我々は共通のコードアーキテクチャを提供するという大きな難局に直面しています。

  • 使いやすく学びやすい
  • 迅速な開発,簡単なデバッグ,痛みを伴わないデプロイ
  • よく組織化され,拡張性があり,保守性がある

JavaScriptはクラスレスのプロトタイプ指向言語です。したがって,本来この言語の最も強力な特徴の1つは柔軟性です。そのため同じ仕事を多くの方法で(コーディングスタイルやテクニックを使って)実装できます。しかし,そのことはコストの予測が困難にもしています。統一された構造がなければJavaScriptコードは理解したり,維持したり,再利用するのが本当に難しい場合があります。 一方,クラスベースのプログラミングは,オブジェクト指向モデルの中で今でも最もポピュラーです。クラスベースの言語にはたいてい強固な型定義,カプセル化,標準的なコーディング規則があります。一般的に、開発者が膨大な規則に準拠して記述したコードは,時間が経っても予測可能で拡張性がありスケーラブルである傾向があります。しかしそれらの言語には,JavaScriptなどの言語で見られるような動的な機能はありません。 どちらのアプローチも一長一短ですが,短所を隠しながら,両方の長所を同時に手にできないのでしょうか。いいえそんなことはありません。Ext JS 4はそれを解決します。

II. 名付け規則

1) クラス

  • クラス名は英数字のみで記述します。数字は許されていますが,テクニカルタームでない限り使わない方がいいです。アンダースコアやハイフンなどの英数字以外の文字は使ってはいけません。例:
    • MyCompany.useful_util.Debug_Toolbar これはNGです。
    • MyCompany.util.Base64 これはOKです。
  • クラス名は,オブジェクトのプロパティのドット表記をうまく使って,パッケージごとにグループ化する必要があります。ユニークなトップレベルのネームスペースは一つだけにして,それにクラス名を続けます。例:
    • MyCompany.data.CoolProxy
    • MyCompany.Application
  • トップレベルのネームスペースと実際のクラス名はアッパーキャメルケース(CamelCase)で書き,それ以外は全て小文字で書きます。例:
    • MyCompany.form.action.AutoLoad
  • Senchaが配布するものでないクラスは,Extをトップレベル名前空間として使わないでください。
  • アクロニム(頭文字語)も同様にアッパーキャメルケースで表記してください。例:
    • Ext.data.JSONProxyではなくExt.data.JsonProxy
    • MyCompary.parser.HTMLParserではなくMyCompany.util.HtmlParser
    • MyCompany.server.HTTPではなくMyCompany.server.Http

2) ソースファイル

  • クラスの名前は保存されているファイルパスにひもつけられます。結果として一つのファイルには一つのクラスだけが存在するということになります。例:
    • Ext.util.Observable は path/to/src/Ext/util/Observable.js に保存されます。
    • Ext.form.action.Submit は path/to/src/Ext/form/action/Submit.js に保存されます。
    • MyCompany.chart.axis.Numeric は path/to/src/MyCompany/chart/axis/Numeric.js に保存されます。
    • /path/to/srcは,そのアプリケーションにおいてクラスが保存されるディレクトリです。すべてのクラスが一つのディレクトリ下にあって,適切なネームスペースに配置されていると,デプロイやメンテナンスのためになります。

3) メソッドと変数

  • クラス名と同様,メソッドや変数の名前も英数字のみで表記します。数字は許されていますが,テクニカルタームでない限り使わない方がいいです。アンダースコアやハイフンなどの英数字以外の文字は使ってはいけません。メソッドと変数の名前は常にキャメルケース(camelCase)で書きます。これはアクロニムにも適用されます。例:
    • 許容できるメソッド名:
      • encodeUsingMd5()
      • getHTML() ではなく getHtml()
      • getJSONResponse() ではなく getJsonResponse()
      • parseXMLContent() ではなく parseXmlContent()
    • 許容できる変数名:
      • var isGoodName
      • var base64Encoder
      • var xmlReader
      • var httpServer

4) プロパティ

  • クラスのプロパティ名は上記のメソッドや変数とほぼ同じ規則に従います。例外はそれが静的定数の場合です。
  • 定数を格納する静的なクラスプロパティは,すべて大文字で記述します。例:
    • Ext.MessageBox.YES = "Yes"
    • Ext.MessageBox.NO = "No"
    • MyCompany.alien.Math.PI = "4.13"

II. ハンズオン

1. 宣言

1.1) 以前の方法

以前のバージョンのExt JSでは,Ext.extendでクラスを作成するのになれていたと思います。

var MyWindow = Ext.extend(Object, { ... });

このアプローチは他のクラスを継承して新しいクラスを生成するわかりやすい方法です。しかし直接継承するのではない場合,configrationとかstaticsとかmixinのような局面をうまく解決するAPIがありませんでした。

もう一つの例を見てください。

My.cool.Window = Ext.extend(Ext.Window, { ... });

このサンプルでは,拡張した新しいネームスペースにExt.Windowクラスを定義してます。ここでは二つのことに注意する必要があります。

  1. My.coolは,WindowプロパティにExt.Windowをアサインする前にすでに存在しているオブジェクトでなければなりません。
  2. Ext.Windowsは,それが参照される前に存在し,ロードされている必要があります。

最初の項目は,Ext.namespace(別名Ext.ns)で解決できます。このメソッドはオブジェクトやプロパティのツリーを再帰的に走査して,まだ存在していなければ作成します。Ext.extendにおいては常に,ネームスペースを追加することを忘れないようにしないといけないのが面倒です。

Ext.ns('My.cool');
My.cool.Window = Ext.extend(Ext.Window, { ... });

しかし二つ目の項目は,そう簡単には解決しません。Ext.Windowは他の多くのクラスと直接,間接に継承していて依存関係があるでしょうし,そのクラスがまた他のクラスに依存するでしょう。そのため,Ext JS4以前のバージョン向けのアプリケーションでは,フレームワーク中の一部の機能を使うだけであってもext-all.jsというライブラリ全体を含んだファイルをインクルードしています。

1.2) 新しい方法

Ext JS4ではただ一つのメソッドExt.defineでそれらのすべての欠点を排除します。クラスを作成するのに覚えなければならないのはこれだけです。基本的な構文:

Ext.define({String} className, {Object} members, {Function} createdCallback);
  • className: クラス名
  • members はキーと値のペアでクラスメンバのコレクションを表わすオブジェクトです。
  • createCallbackはオプション関数で,このクラスのすべての依存関係が解決され,クラス自身が完全に生成された時にコールバックされます。新しいクラス作成は非同期的な性質なので,このコールバックは多くの状況で役に立ちます。

例:

Ext.define('My.sample.Person', {
    name: 'Unknown',

    constructor: function(name) {
        if (name) {
            this.name = name;
        }

        return this;
    },

    eat: function(foodType) {
        alert(this.name + " is eating: " + foodType);

        return this;
    }
});

var aaron = new My.sample.Person("Aaron");
    aaron.eat("Salad"); // alert("Aaron is eating: Salad");

2. コンフィグレーション

2.1) 以前の方法

これまではクラスプロパティとユーザー設定コンフィグの違いを見分けるしっかりした方法がありませんでした。コンフィグは@cfgアノテーションを使ってクラスの正常なプロパティとして定義され文書化されます。次のサンプルクラスを見てください。かなり長いのですが,この問題についてよくわかります。

Ext.ns('My.sample');
My.own.Window = Ext.extend(Object, {
   /** @readonly */
    isWindow: true,

   /** @cfg {String} title The default window's title */
    title: 'Title Here',

   /** @cfg {Object} bottomBar The default config for the bottom bar */
    bottomBar: {
        enabled: true,
        height: 50,
        resizable: false
    },

    constructor: function(config) {
        Ext.apply(this, config || {});

        this.setTitle(this.title);
        this.setBottomBar(this.bottomBar);

        return this;
    },

    setTitle: function(title) {
        // Change title only if it's a non-empty string
        if (!Ext.isString(title) || title.length === 0) {
            alert('Error: Title must be a valid non-empty string');
        }
        else {
            this.title = title;
        }

        return this;
    },

    getTitle: function() {
        return this.title;
    },

    setBottomBar: function(bottomBar) {
        // Create a new instance of My.own.WindowBottomBar if it doesn't exist
        // Change the config of the existing instance otherwise
        if (bottomBar && bottomBar.enabled) {
            if (!this.bottomBar) {
                this.bottomBar = new My.own.WindowBottomBar(bottomBar);
            }
            else {
                this.bottomBar.setConfig(bottomBar);
            }
        }

        return this;
    },

    getBottomBar: function() {
        return this.bottomBar;
    }
});

要するに,My.own.Windowは,

  • インスタンス化の時コンフィグオブジェクトを受け入れ,クラスのデフォルトプロパティとマージします。
  • titleとbotttomBarはセッターによって実行時に変更できます。

このアプローチには一つの利点がありますが,同時にそれは欠点でもあります。インスタンス時に上書きされてはならないプライベートメソッドやプロパティを含めクラスインスタンスの全てのメンバーを上書きできます。このフレキシビリティを考えたカプセル化の妥協は,これまで多くのアプリケーションにおいて誤用のもとになっていました。誤用されるとデバッグしにくく保守性に問題のあるプアなデザインになってしまいます。

さらに,他の制約もあります。

  • Ext.applyはオブジェクトのプロパティを再帰的にマージしません。したがって,この例ではbottomBarの他の既定のプロパティを指定せずに,例えばbottomBar.heightを60にオーバーライドするだけということはできません。
  • ゲッターとセッターは全てのコンフィグプロパティに自分で定義しなければなりません。どのプロパティがコンフィグであるかが明確ではないので,セッターやゲッターを自動的に生成することはできないのです。

2.2) 新しい方法

Ext JS 4では,クラスが生成される前に、強力なExt.Classプリプロセッサが処理する,専用のコンフィグプロパティがあります。上記の例をそれで書き直しましょう。

Ext.define('My.own.Window', {
   /** @readonly */
    isWindow: true,

    config: {
        title: 'Title Here',

        bottomBar: {
            enabled: true,
            height: 50,
            resizable: false
        }
    },

    constructor: function(config) {
        this.initConfig(config);

        return this;
    },

    applyTitle: function(title) {
        if (!Ext.isString(title) || title.length === 0) {
            alert('Error: Title must be a valid non-empty string');
        }
        else {
            return title;
        }
    },

    applyBottomBar: function(bottomBar) {
        if (bottomBar && bottomBar.enabled) {
            if (!this.bottomBar) {
                return new My.own.WindowBottomBar(bottomBar);
            }
            else {
                this.bottomBar.setConfig(bottomBar);
            }
        }
    }
});

And here’s an example of how it can be used:

var myWindow = new My.own.Window({
    title: 'Hello World',
    bottomBar: {
        height: 60
    }
});

alert(myWindow.getTitle()); // alerts "Hello World"

myWindow.setTitle('Something New');

alert(myWindow.getTitle()); // alerts "Something New"

myWindow.setTitle(null); // alerts "Error: Title must be a valid non-empty string"

myWindow.setBottomBar({ height: 100 }); // Bottom bar's height is changed to 100

これらの変更によって

  • My.own.Windowクラスのコード量が減り,機能的になりました。
  • コンフィグレーションは他のクラスのメンバーから完全にカプセル化されます。
  • すでに定義済みでなければクラスが生成されるときに,すべてのコンフィグプロパティに対するセッターとゲッター,それにapply*とrest*というメソッドが,クラスのプロトタイプの中に自動的に生成されます。

これ以降は,2011/04/28現在まだ執筆されていないようです。