Titanium でクラスっぽいモノを書くときに気をつけること

(追記 : 2012/05/31 9:53)
JavaScript のコンストラクタ関数で new で生成したオブジェクトや関数を返却すると prototype で拡張したメソッドを辿れなくなるのは JavaScript の仕様でした。ただ、 Titanium オブジェクトが JavaScript のオブジェクトではないとされているので、検証をしてみないとよく分からない領域でもあります。

(追記2 : 2012/05/31 11:20)
コンストラクタ関数で tabGroup を返却しているのだから、 new 演算子の式で得られる結果は Ti.UI.TabGroup オブジェクトでした。 TabGroup そのものを prototype で拡張していないので、 AppTabGroup.prototype で拡張したメソッドを呼べないのは当然の結果と思います。誤解を招く記事になってしまい、ご迷惑をおかけしました。

Titanium Mobile は JavaScript を使って iOS や Android などモバイルプラットフォーム向けのネイティブアプリケーションを構築することができるツールキットです。

JavaScript はプロトタイプベースのオブジェクト指向言語ですが、言語仕様上まるでクラスベースであるかのような文法が見受けられるために混乱を招きがちです。そのため、 JavaScript そのものを糖衣する CoffeeScript ではクラスベースとして振る舞うような文法が採用されています。

クラスベースのオブジェクト指向言語に慣れた人からすれば、 JavaScript のプロトタイプに頭を抱えることなく Titanium Mobile アプリケーション開発を行える CoffeeScript  は有益な言語です。しかし、 Titanium Mobile アプリケーション開発を行う際の注意点を知っておかないと単純な「クラスっぽいモノ」を書くときにはまることがあります。これは、 JavaScript でも CoffeeScript でも共通していえることです。

今回はこの注意点を説明します。また、現在の JavaScript のクラスとその他の言語のクラスは別物ですので、この記事中では「クラスっぽいモノ」と表現します。

まず始めに、 JavaScript でクラスっぽいモノを書く方法の一つに「コンストラクタ関数を書いて、メソッドは prototype を拡張して実装する」があります。

こんな感じです。より良い方法はオライリーの JavaScript: The Good Parts をご覧になると良いと思いますが、基本的にはこのような形でクラスっぽいモノを定義します。このコードを CoffeeScript で表すと、

ですね。とても単純な例なので行数に殆ど違いはありません。コンストラクタ関数は便宜上「コンストラクタ」の名前を付けているだけで、動きは通常の関数を変わりません 。ただ、通常の関数との違いを表現するために名前の頭を大文字で表現することが通例となっています。また、コンストラクタ関数は通常返り値を設定しません。

Titanium Mobile でこの「クラスっぽいモノ」を活用すると、役割に応じてソースコードをモジュールに分割できるので、アプリケーション開発が楽になります。例えば、

app.js / AppWindow.js / AppTabGroup.js の3つにコードを分割しました。これらのコードは全て同じディレクトリに置かれているものとします。
AppWindow.js と AppTabGroup.js はコンストラクタ関数だけが定義されていて、これを module.exports によって CommonJS モジュールにしています。

AppTabGroup.js と AppWindow.js は返り値を定義しています。 new 演算子でコンストラクタ呼び出しを行った結果として、返り値が呼び出し元の変数にセットされます。例えば AppTabGroup 関数は Ti.UI.tabGroup オブジェクトを返します。この Titanium Mobile アプリケーションをビルドし、実行すると2つのタブを持つアプリケーションが立ち上がります。

それではアプリケーションを拡張して、それぞれのタブに属している window 内にテキストフィールドとボタンを設置し、テキストフィールドに入力した値に応じてタブの名前が変わるようにしてみましょう。

説明のため、くどい書き方になりました。

表示されているボタンをタップすると、 fireEvent メソッドによってテキストフィールドに入力されている値を持って changeTabName イベントを発火させます。このイベントを取り回す無名関数の中では現在選択されているタブが持つ tabId と、受け取っているテキストフィールドの入力値を引数に app.tabGroup が持っているはずの changeTabName メソッドを呼び出しています。

メソッドの処理が無事に終了すれば、タブの名前は入力されている値に変わるはずです。しかし、このメソッドを呼び出すとエラーが出力されます。

どうやら呼び出したはずの changeTabName メソッドが undefined のようです。なぜ定義したはずのメソッドが undefined になっているのでしょうか? 正解は Appcelerator 社の Coding Best Practices 内にある「Don’t Extend Titanium Prototypes」に書かれています。 搔い摘むと「Titanium オブジェクトは JavaScript のオブジェクトじゃ無くて実際にはネイティブ API とのプロキシだから拡張するなよ」と、いうことです。

AppTabGroup 関数 (コンストラクタ関数) の中で this.tabGroup を返却しています。この this.tabGroup は Ti.UI.createTabGroup メソッドで作られた Titanium オブジェクトです。 Titanium オブジェクトを返す関数に prototype で拡張したメソッドを追加しても、 Titanium オブジェクトの拡張は禁止されているので呼び出すことができなくなってしまいます。

それでは Titanium オブジェクトをベースにした便利メソッドを作ることはできないのでしょうか? 答えは簡単です。 Titanium オブジェクトを返却しなければ良いのです。例えば、先の例を正しく動かしたかったら AppTabGroup.js を以下のように編集します。

コンストラクタ関数内の return で this.tabGroup を返却することをやめました。すると app.js 内の app.tabGroup の値は Titanium オブジェクトではなくなるので open メソッドが未定義になります。このままだと動きませんので、 AppTabGroup を拡張して open メソッドを定義しておきます。このメソッド内で this.tabGroup.open() を実行することでアプリケーションを正しく動作させることができます。

この書き方によって、テキストフィールドに入力した値をタブの名前に反映させることができました。 CoffeeScript を使う場合でも同じですが、 CoffeeScript には関数やメソッド・条件判断などで最後に定義されたものを自動的に return させる仕組みがあります。なので AppTabGroup を定義するには明示的に返り値を持たせない return を書く必要があります。

このような形ですね。もしも return 文を書かないと、最終行に書かれた内容 (@tabGroup.add 〜 ) が return されてしまいます。なのでただ return とだけ書きます。

この「Titanium オブジェクトを拡張してはいけない」という事を頭に入れておけば、 JavaScript でも CoffeeScript でもクラスのようなモノを定義してプログラムを柔軟に書いたり、 CommonJS モジュールを作ったりすることができるようになります。

例えば、 Ti.UI.tabGroup.open メソッドを呼び出すときに、デフォルトのタブを明示したい場合は AppTabGroup.prototype.open メソッドを以下のように拡張します。

CoffeeScript では以下のように表せます。

このようにしておけば、 app.js の中で app.tabGroup.open メソッドを呼び出すときに引数として数値型で適当な値を渡すことで、一番最初に開きたいタブを決定することができます。

Titanium オブジェクトそのものの拡張ができないので、これをラップする関数やメソッドを定義しなくてはいけないのがもどかしいところではありますが、その分、付随処理を行ったりコールバックを定義したりできるようになるので、より柔軟に Titanium Mobile アプリケーション開発を行えることでしょう。

Code Strong !

おまけ。 CoffeeScript でクラスっぽいモノを書いて JavaScript にコンパイルすると結構凄い JavaScript がはき出されます。

これが…

こうなります。確かにこうすればクラスメソッドやクラス変数を定義できますね。