2012年3月27日火曜日

Viewはshowしたときに描き直されるもの?

Titanimu Mobileを使って、新しいアプリを開発中です。いろいろと試しながらの開発なので、実際に作る工程よりも、どんな動きをするのか細かな部分の解析に時間を取られています。使い始めて2つめのアプリなので、仕方ないでしょうね。

 

今回は、Viewの描画で困ったことが起こりました。普通にViewを作成して、普通に表示しているだけなら、とくに問題はありません。でも、少し凝った機能が必要になって、ViewにaddしたUI部品のレイアウトを途中で何度も変更する機能を実装しました。UI部品のtopやleftを途中で変更し、1つのViewのままで、複数のレイアウトを実現する機能です。メモリー効率も良さそうですし。

さすがに、表示したままレイアウトを変更するわけにはいきません。Viewをいったんhideして、hideした状態でUI部品のレイアウトを変更し、変更が終わってからViewをshowしたら問題ないだろう考えました。他の工夫(viewをhideしても問題が生じない工夫)と組み合わせる必要はありますが、Viewに関するJavaScriptは、次のようになります。

view1.hide();      // view1を非表示にします
label1.top = 40;   // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();      // view1を再表示します

これが期待した動きになりませんでした。UI部品のレイアウトを変えてから、Viewをshowした場合は、ほんの一瞬だけ変更前のレイアウトが表示され、直後に新しいレイアウトを描いて安定します。フラッシュバックで一瞬だけ古い画像が表示されたような感じです。バグでしょうか、仕様なのでしょうか。こんな使い方をしている人がいないためか、検索しても情報は見付かりませんでした。普通に考えると、表示する内容を内部で作り終わってから、画面に描画するように作るのではと思います。でも、動きから推測するに、そうなってはいないようです。

真剣に困りました。この方法が使えないと、複数のレイアウトのViewを用意しておき、それぞれに値を設定し直してから切り替える必要があります。レイアウトを増やす場合も、新しいViewを追加しなければなりません。美しくないですね。何とか回避できないかと、かなり悩みました。

悩んだ結果、1つ思い付きました。透明度を変えるopacityを使えないかと。opacityの値で限りなく透明にした状態でViewを描かせ、直後に不透明状態に戻せば、フラッシュバックが消えるのではないかと。完全に透明にしてしまうと、描く処理が開始しないと思い、限りなく透明な値として0.001を選んでいます。さっそく、次のようなJavaScriptで試しました。

view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();          // view1を再表示します

結果は、惨敗でした。何も変わらず、フラッシュバックが再現されました。まだ、あきらめません。限りなく透明にする位置が悪いのではないかと考えました。hideしてから限りなく透明にしても、その状態を描いていません。限りなく透明にするのを、hideする前に行えば、表示されている時間内に限りなく透明で描いたことになり、有効だと考えました。試したJavaScriptは、次のような形です。

view1.opacity = 0.001; // view1を限りなく透明にします
view1.hide();          // view1を非表示にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();          // view1を再表示します

またまた惨敗しました。ここでいったん敗北宣言です。こんなときこそ、美味しい珈琲を飲んで休息です。時間を置いてから、再び考えました。もしかしたら、時間差攻撃が有効かもと。Viewをshowした直後にopacityを戻しているからダメなので、少し時間を経過してからなら大丈夫ではないかと。限りなく透明に設定する処理をhideの後ろに戻し、次のJavaScriptで試しました。

view1.hide();          // view1を非表示にします
view1.opacity = 0.001; // view1を限りなく透明にします
label1.top = 40;       // view1上のUI部品のプロパティを変更して、画面上のレイアウトを変えます
label1.left = 30;
imgView.top = 120;
...
view1.show();                  // view1を再表示します
setTimeout(resetOpacityF, 50); // 50ms後に、不透明に戻すfunctionを起動させます
// この関数は、ここで終了

function resetOpacityF(){      // 時間差攻撃で、view1を不透明に戻します
    view1.opacity = 1;
}

今度は、大成功でした。フラッシュバックがまったく出ません。勘で決めた50msですが、時間差攻撃が成功です。最近の高速コンピュータにとっては、50msでも長い時間なのでしょう。もっと短くなるかも知れませんが、不安が増すので今のところ50msで固定です。

以上は、シミュレータでの動きでした。実機ではフラッシュバックが出ないかも知れませんし、この解決方法で実機も大丈夫という保障もありません。ただし、show後に描き直しているという動きから考えて、非常に有効な解決方法です。

開発の基本としては、実機でもシミュレータでも正常に動作することが大事です。実機で動いたからといって、その実機だけかも知れません。機種もOSバージョンも違う環境が、何種類も存在します。すべての環境でテストすることは無理なので、実機でもシミュレータでも動くことを、最低条件とするわけです。また、問題の解決にあたっては、たまたま直ったのではなく、こういう現象だからこうすれば直るはずと、理論的な裏付けのある解決方法が大事です。

今回の問題は苦労しましたが、何とか解決できて良かったです。まだ実機でのテストが残っていますが、最悪の場合でも、時間差の数値を変更するだけで大丈夫でしょう。複数レイアウトのViewを用意することだけは、ぜったいにやりたくないですからね。

 

Titanimu Mobileの経験が浅いので、もしかしたら、別な解決方法があるかも知れません。ご存じのことがいたら、ぜひ教えてください。一応、viewのプロパティなども本家のAPIページで全部見ましたが、それらしいものは見付かりませんでした。唯一使えそうだったのが、opacityというわけです。

2012年3月21日水曜日

シングルコンテキストでの設計方法(6)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第6回です。作り方をひととおり説明しましたが、これから主流となりそうな「require」の話を取り上げないわけにはいきません。ということで、「require」を使って作る話です。

 

まず最初に、これまでの経緯を簡単に。Titanium Mobileを使うのが初めてだったので、いろいろ調べました。「Ti.include」を使う方法と、「require」を使う方法の2種類があって、将来的には「require」へ移行するという話のようでした。どちらで作るか悩んだのですが、「Ti.include」を使うほうが簡単そうに見えたのと、「Ti.include」も経験したほうが良いだろうとの考えもあり、最初は「Ti.include」で作って、必要なら後から「require」に書き換えればよいかなと判断しました。

実は、作成した業務アプリ、最初はTitanium Desktpoで作成したものです(こちらも、Titanium Desktpoを使うのが初めてでした)。それをiPadに移植しようと、Titanium Mobile用に修正しながら作りました。予想したよりも短期間でアプリは出来上がり、依頼主に見せたところ、以前見せたデスクトップ版よりも、iPad版を速攻で気に入ってしまいました。短期間だけ試してもらった後、依頼主の要望を反映し、機能を少し追加しました。アプリが非常に安定していたため、すぐに本番で使い始めることに。本番で使い始めたら、安定している業務アプリを修正する気にはなりません。こうして「Ti.include」のまま、アプリは本番で使い続けられています。運が良かったのか、トラブルはまったく出てません。

こんな感じで、「require」に書き換える機会を失ってしまいました。とりあえず「Ti.include」で作るかという、私の直感的な(?)判断が招いた結果ですね。「Ti.include」も分かりやすい形で作れるから、なかなか好きなのですが。でも将来は使わないと言われたら、「require」へ移行できるように試しておかないとダメでしょうね。ちゃんと調べて、作り方も決めてあります。

 

いよいよ、ここからが今回の本題です。「require」を使う場合も、今回の3つのレベル分けは同じです。レベル2とレベル3に1文字ずつ割り当てる考え方も、そのまま生かします。実際にJavaScriptをどう作るのか、順番に見ていきましょう。なお、作り方が何種類から選べるため、私が気に入った作り方を選びました。選んだ理由も、少し加えながら解説します。

まずは、レベル1のJavaScriptです。Ti.include方式では全体を即時関数で作りましたが、これは最初にインクルードされたときに実行して、グローバル変数bbに登録するためでした。require方式でも、グローバル変数に登録しますが、登録方法が異なります。そのため、全体を即時関数にする必要はなくなりました。また、このJavaScriptの即時関数の外で、グローバル変数bbを宣言していましたが、これは別な場所に移動しました。JavaScript自体は、次のような形です。

// base.js (レベル1)
exports.createWinF = function(_title){
    return Ti.UI.createWindow({
        title:_title,
        backgroundColor:'#fff'
    });
}
exports.createLblF = function(_text, _fontSize, _textAlign, _height, _width, _top, _left){
    return Ti.UI.createLabel({
        text:_text,
        font:{fontSize:_fontSize},
        textAlign:_textAlign,
        height:_height,
        width:_width,
        top:_top,
        left:_left 
    });
}
exports.createBtnF = function(_title, _fontSize, _height, _width, _top, _left){
    return Ti.UI.createButton({
        title:_title,
        font:{fontSize:_fontSize},
        height:_height,
        width:_width,
        top:_top,
        left:_left
    });
}

Ti.include方式で「dd.createWinF = 」としていたものが、require方式では「exports.createWinF = 」と変えてあります。require方式で外から呼ばれる関数なので、「exports」が必須となります。それを利用する方法ですが、次のような形にしました。

// app.js (メインのJavaScript)
var bb = {}; // グローバル変数
var bbb = {};          // レベル1専用のグローバル変数
bbb = require("base"); // レベル1を使えるようにする
(function() {
    // メインのウィンドウを開く
    bb.win = bbb.createWinF('prod_edit');
    var lblTitle = bbb.createLblF(bb.win, '主メニュー', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bbb.createLblF(bb.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnOpenPe = bbb.createBtnF(bb.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
})();

一番の違いは、レベル1専用のグローバル変数を用意したことです。「bb.func_name = 」という形で追加できなくなり、全体を直接「bb」に入れる「bb = require("base");」という形になります。これだと追加するのではなく、置換えになります。一番最初なので置換えでも、後の追加まで正常に動くのでしょうが、気分的にイヤと感じました。そこで別なグローバル変数「bbb」を用意して、そこに入れた(「require」した)わけです。

当然ですが、レベル1をグローバル変数に入れなくても使えます。その場合は、レベル1を使うJavaScript全部で、先頭に「bbb = require("base");」と書かなければなりません。これが面倒なので、グローバル変数に入れたわけです。面倒だと思わなければ、毎回書いても良いでしょうね。

 

続いて、レベル2のJavaScriptです。これも全体を即時関数で囲む必要はありません。また、先頭で入れ物「bb.c」を用意することも不要です。その結果、JavaScriptは次のようになります。

// c_customer.js (レベル2)
// 継続保持させるデータ用の変数
var customar = [];
var custType = [];

// 外部からデータにアクセスするための関数
exports.initDataF = function(id) { ... } // 初期処理
exports.getNameF = function(id) { ... }
exports.addCustomerF = function(name, ... ) { ... }
exports.checkCstmIdF = function(id) { ... }
// 外部からアクセスされない関数
function calcYearF(date) { ... }
// 上記2種類の関数は、別々に分けるのではなく、関係の深いものを近付ける形で混在させる

値を継続的に保持したい変数を宣言して、後に関数を続けます。外部からアクセスする関数だけ、「exports.func_name = 」という形にして、残りは普通の関数として書きます。

Ti.include方式とは違い、使う側で準備が必要です。複数のJavaScriptから使われるため、毎回宣言するのも面倒と思い、メイン処理でグローバル変数に登録してしまいました。

// app.js (メインのJavaScript)
var bbb = {};          // レベル1専用のグローバル変数
bbb = require("base"); // レベル1を使えるようにする
var bb = {};                   // グローバル変数
bb.c = require("c_customer"); // レベル2を使えるようにする
bb.c.initDataF();              // まずは初期化
bb.p = require("p_products");  // 別なレベル2を使えるようにする
bb.p.initDataF();              // 同じく初期化
(function() {
    // メインのウィンドウを開く
    bb.win = bbb.createWinF('prod_edit');
    var lblTitle = bbb.createLblF(bb.win, '主メニュー', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bbb.createLblF(bb.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnOpenPe = bbb.createBtnF(bb.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
})();

レベル2の数だけ、グローバル変数「bb」に登録します。これでレベル3のJavaScriptでは、宣言なしで使えるようになりました。使い方もTi.include方式と同じで、「bb.c.func_name()」という形になります。

 

続いて、最後のレベル3です。レベル3は関数が外部から呼ばれない点で、レベル1やレベル2とは異なります。レベル3は、レベル1やレベル2を利用する側だからですね。「Ti.include」で呼ばれるため全体を即時関数にしていましたが、require方式では「exports」で外から呼ばれる方式に変わります。全体が即時関数の形に似せて、全体を1つの「exports」関数に作りました。即時関数と同様に、全部の変数が関数の内側に入る点が好きだからです。これとは違って、普通に関数を分割して書き、オープンする関数だけ外から見えるようにしても構いません。

// pe_prod_edit.js (レベル3)
exports.openWin = function() {
    // pe用の入れ物を用意する(ウィンドウ用)
    bb.pe = {};

    // 画面に表示するためのUI部品
    bb.pe.win = bbb.createWinF('prod_edit');
    var lblMnTitle = cc.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnClose = bb.createBtnF(bb.pe.win, '終了', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', closeWinF);

    // データを内部的に保持するための変数
    var prodScrn = [];
    var idxProd = null;
    var userId = null;

    // イベントを実行したり、データを加工する関数
    function openWinF() { ... }
    function saveDataF() { ... }

    // ウィンドウを閉じる処理
    function closeWinF(){
        bb.pe.win.close(); // ウィンドウを閉じて、
        bb.pe = null;      // UI部品をウィンドウごと解放する
    }

    // 最後に、生成済みのウィンドウを開く
    bb.pe.win.open();
}

関数は呼ばれないので考慮する必要はないのですが、ウィンドウを開く処理や閉じる処理をどのように配置するのか、決めなければなりません。この例の作り方では、JavaScript全体がオープン用関数になっていて、そのなかでウィンドウの生成なども行います。同時に、ウィンドウ上のUI部品へイベント処理を加え、その処理から呼ばれる関数も用意します。それらが終わった最後に、生成したウィンドウを開いています。

ウィンドウを終了するときのために、画面を終了するボタンとして付けてありますが、「戻る」ボタンであったり、何かの処理が終わった直後に閉じる場合もあるでしょう。とにかく、ウィンドウを閉じる機能が必要です。ウィンドウを閉じる関数では、ウィンドウを閉じた後に入れ物「bb.pe」を空にして、作成したウィンドウごと消えるようにしています。メモリー解放のためです。

このように作った画面用JavaScriptは、次のような形で呼び出します。

menu.js (呼び出しボタンの付いた画面のJavaScript)
(function() {
    ...
    var btnOpenPe = bbb.createBtnF(bb.xx.win, '商品編集', 24, 40, 200, 100, 32);
    btnOpenPe.addEventListener('click', openPeWinF);
    ...
    // 商品編集の画面を呼び出す関数
    function openPeWinF(){
        require("pe_prod_edit").openWin();
    }
})();

ウィンドウを呼び出すための関数では、JavaScript「pe_prod_edit.js」を「require」して、ウィンドウを作って開く関数を呼び出します。分かりやすいように別関数としましたが、たった1行のJavaScriptです。

 

以上のように、require方式でも、3つのレベルに分けた設計方法が使えますし、Ti.include方式と同じメリットがあります。世の流れとしてrequire方式に動いている以上、これから作るならrequire方式でしょう。Ti.include方式は、意外にスッキリした構造で好きなんですけどね。というわけで、新しく作る場合は、require方式で作ってください。私も次のアプリを、require方式で作り進めています。将来を約束されている方式で作っておかないと、やっぱり多少は不安がありますからね。

今回で、設計方法の話はいったん終りです。私は凄く気に入っている方法ですが、感じ方は人それぞれだと思います。部分的にでも気に入った点があったら、そこだけでも利用してください。全体の設計以外にも、iPadアプリを作っていて気付いた点がありますから、ぼちぼち書き進めます。

2012年3月19日月曜日

シングルコンテキストでの設計方法(5)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第5回です。3つのレベルに分けてJavaScriptを作る、という話で進めてます。今回は3つめとなるレベル3の作り方を取り上げますね。

 

レベル3のJavaScriptに入れるのは、MVCモデルのVCに含まれる内容です。基本的には、アプリで使う画面ごとに分けて作ります。画面ごとに2文字以上の英字を割り当てて、それを先頭に入れたファイル名としてJavaScriptを別々に用意するルールでした。たとえば「pe_prod_edit.js」ように。どのJavaScriptも、次のような形になります。

// pe_prod_edit.js (レベル3)
(function() {
    // 名前空間を別にするために、peの入れ物を用意する
    bb.pe = {};

    // 画面に表示するためのUI部品
    bb.pe.win = bb.createWinF('prod_edit');
    bb.pe.win.addEventListener('open', openWinF);
    var lblTitle = bb.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);

    // データを内部的に保持するための変数(bb.pe.を付けなくても、varで動くはずですけど...)
    bb.pe.prodScrn = [];
    bb.pe.idxProd = null;
    bb.pe.userId = null;

    // イベントを実行したり、データを加工する関数
    function openWinF() { ... }
    function saveDataF() { ... }
})();

シングルコンテキストの作り方に沿って、これもJavaScript全体を即時関数で作ります。最初がpeの入れ物で、名前空間を別々にするために用います。ここで作る変数も関数もすべてpeに登録しますから、名前が重複しないようにと調べなくて大丈夫です。

次は、画面表示に使うUI部品です。レベル1のJavaScriptで用意した関数を使い、Titanium MobileのUI部品を加えていきます。最初に作るウィンドウだけはbb.pe空間に付けますが、それ以外のUI部品はvarの変数で作って、ウィンドウにaddするだけです。この例では、addまで含んだレベル1の関数を使い、UI部品を生成しています。ウィンドウだけbb.pe空間に付けますが、「bb.pe.win」と短い表記で済むため、ソースコードが長くなりません。他の画面のウィンドウでも「win」と短い名前に統一すれば、「bb.pv.win」や「bb.ce.win」となって、同じように短い名前で使えます。

続いて、このJavaScript内で使うデータ用変数のうち、値を継続して保持する必要がある変数だけ、bb.pe空間に付けます。これも前回と同じで、おまじない的に付けています。本来は、変数がどこかで必ず参照されていて、bb.pe空間に付けなくても動作するはずなのですが、、、、。前回と同様に苦肉の策なので、この部分だけは非推奨ということで。

保持して置く場所として、変数以外に画面表示も使えます。ユーザーが選んだ値は、画面上のUI部品に保持しています。その値を調べることで、変数に持つ必要はなくなります。また、UI部品が保持している値がそのまま使えないときでも、UI部品に新しいプロパティを加えて、そのプロパティに値を保持させる方法が使えます。どちらの方法でも、JavaScript内の変数を持たなくても、持ったと同じ効果が得られます。こういう方法も利用して構わないのではないでしょうか。

レベル3で作る関数は、他のJavaScriptから呼ばれるものは、めったにありません。他の画面と共有する関数はレベル1として作り、その関数へウィンドウやボタンの情報を渡して動かす形にします。そうすると、このJavaScript内で使う関数しか残らないのです。したがって、どれも普通の関数「function func_name() { ... }」の形で作ることになります。

 

外から呼ばれる関数がないので、ここで作った関数を使うJavaScriptの例もありません。当然ですね。逆に、ここの関数内では、レベル1やレベル2で作った関数を多用します。それらを使って、データの作成、加工、保存を実現します。また、新しく作成するデータは、画面に入力した値を取り込んで利用します。

画面上でのエラーチェック機能は、このJavaScriptに入れます。ただし、エラーを判定する処理だけは、レベル1またはレベル2に関数として用意します。その関数を呼び出してエラーかどうか判定し、判定結果を画面に表示します。エラー判定関数をレベル1に作るか、レベル2に作るかは、エラーチェック内容によって決まります。適用業務に関係するならレベル2に、関係しないならレベル1に作るのが基本です。特定の形式の文字列かどうかのチェックなら、適用業務に関係ないのでレベル1の関数となります。逆に適用業務上のエラーチェックは、該当するデータのレベル2JavaScriptに入れます。データごとに、関係するロジックがレベル2の該当箇所に集まるというわけです。

より良い構造で作るのであれば、レベル1のエラーチェック関数を、レベル3から直接呼ばない形が理想です。たとえ特定の形式の文字列かどうかのチェックであっても、チェック目的を表す名前でレベル2に関数を用意し、その中でレベル1のエラーチェック関数を呼ぶようにします。レベル1の関数名は、数値かどうかのチェックを意味する名前に、レベル2の関数名は、どのデータ項目をチェックするか表す名前になるでしょう。このように作れば、レベル2のJavaScriptの中に、エラーチェック内容がすべて集まる形に仕上げられます。

 

こんな感じで作り進みます。作業の順序としては、名前空間の入れ物を用意して、UI部品を作り、内部保持の変数を加えて、関数を書いていく感じですね。必要に応じて、UI部品の表示位置を調整したり、関数の修正や追加を行います。

以上で、3つのレベルの作り方を紹介し終わりました。ここまで書いてきて、レベルという表現(言葉)が適切だったのか、少し迷ってます。JavaScriptを3層構造に分けるという意味で、レベルかなと考えましたが、はたして良かったかどうか。適切な言葉って、意外に難しいですね。

シングルコンテキストでの作り方を調べたとき、将来的に「Ti.include」が使われなくなって、「require」を使う話がありました。最後に、「require」の話を別投稿で取り上げましょう。

2012年3月18日日曜日

シングルコンテキストでの設計方法(4)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第4回です。です。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、その中のレベル2の作り方を取り上げます。

 

レベル2のJavaScriptに入れるのは、MVCモデルのMに含まれる内容です。適用業務で扱うデータを、種類ごとに分けて作ります。顧客情報、商品情報、販売情報など、情報ごとに分けて別々のJavaScriptとして作ります。それぞれに英字1文字を割り当て、その文字を先頭に入れたファイル名を付けるというルールでした。たとえば「c_customaer.js」のように。どのデータのJavaScriptも、次のような形になります。

// c_customaer.js (レベル2)
(function() {
    // 名前空間を別にするために、cの入れ物を用意する
    bb.c = {};

    // 継続保持させるデータ用の変数(bb.c.を付けなくても動くはずですけど...)
    bb.c.customar = [];
    bb.c.custType = [];

    // 外部からデータにアクセスするための関数
    bb.c.getNameF = function(id) { ... }
    bb.c.addCustomerF = function(name, ... ) { ... }
    bb.c.checkCstmIdF = function(id) { ... }
    // 外部からアクセスされない関数
    function calcYearF(date) { ... }
    // 上記2種類の関数は、別々に分けるのではなく、関係の深いものを近付ける形で混在させる
})();

シングルコンテキストの作り方に沿って、JavaScript全体を、即時関数で作ります。最初に用意するのが、cの入れ物です。ここで作る変数も関数もすべてcに登録しますから、他と名前が重複しないように考える必要はありません。JavaScriptの予約語などと重複しないように注意するだけで済みます。Titanium Mobileで使われている名前は、Titaniumオブジェクトに付けてあるので、同じ名前でも重複扱いにはなりませんね。

続いて、このJavaScript内で使うデータ用変数のうち、値を継続して保持する必要がある変数だけ、bb.c空間に付けます。本来は、変数がどこかで必ず参照されていて、bb.c空間に付けなくても動作するはずなのですが、実際に動かすと、ごく一部の変数だけは消えてしまった様子に。もしかしてと思って付けたら、正常に動きました。自分の使い方が悪いのか、そういう動きが正常なのか、未だに不明です。時間がなかったので、ごく一部の変数だけなんですが、また発生して悩むのは時間の無駄でしょう。仕方がないので、値を継続保持する変数だけは、bb.c空間に付けることにしました。もちろん苦肉の策で、この部分だけは非推奨でお願いしますね。

時間がなかったので、とりあえず動くようにして作業を続けました。そしたら、どの変数で発生したのか忘れてしまって、原因追及ができないでいます。コメントで印を付けておくべきでした。まあ、「急いでるから」とか「時間がないから」と先を急ぐときに起りがちな、残念な失敗の一つでしょう。我ながら情けないです。動いているのを変更して探すのもアレなので、とりあえずこのままかな。

レベル2で作る関数には、外部(レベル2上位層とレベル3)から呼ばれる関数と、即時関数の内部からしか呼ばれない関数に分けられます。外部から呼ばれる関数だけはbb.c空間に付けた形で作り、それ以外を普通の関数(function func_name() { ... })として作ります。これらの関数は、内容を理解しやすいように、関係の深い関数を近くに並べるようにします。外部から呼ばれる関数だけ集めて並べる必要はありません。

 

こうして用意したレベル2のJavaScriptの関数は、レベル2上位層とレベル3のJavaScriptで利用します。bb.c空間に付けてるので、それを付けた形で呼び出すだけです。

// ce_customer_edit.js (レベル3)
(function() {
    bb.ce = {};
    bb.ce.win = bb.createWinF('custm_edit');
    ...

    var cstmId = lblCstmId.text;
    if (bb.c.checkCstmIdF(cstmId)) {
        var strCstmName = bb.c.getNameF(numId);
        ...
    }

    ...
})();

用意した空間名「bb.c.」を関数名の前に付けるだけで、それ以外は普通の関数と同じ使い方です。空間名が短いので、それほど邪魔にならないのが救いでしょうか。「bb.c.」すら書きたくない人は、「var c = bb.c」と作れば、「bb.c.」が短い「c.」に置き換えられます。そもそも「bb」は仕方なく付けている接頭語みたいなもので、「c」だけでも意味が通じますからね。私としては、そこまでやる必要がないと思いますけど。

 

レベル2上位層は、レベル3の関数利用とレベル2の関数公開を混ぜたような作り方になります。レベル2で作った関数を、bb.cなどの空間名付きで用いながら、自分が公開する関数を空間名付きで作ります。レベル2上位層にも重複しない空間名が割り当てられているので、関数名を自由に付けられます。レベル2上位層で作った関数の使い方は、レベル2で作った関数と同じです。

ほどほどの長さになったので、続きは別な投稿にしますね。

2012年3月17日土曜日

シングルコンテキストでの設計方法(1)

Titanium Mobileを使い、iPad上で動く業務アプリを作りました。Titanium Mobileを使う最初のアプリだったので、どのような内部構造にするか事前に調べました。出てきたキーワードが、シングルコンテキストです。シングルコンテキストながら、個々のプログラムが影響を及ぼしにくく作るのが、今後の主流になりそうだと感じました。何人もの人が主張しているほど、人気の作り方のようです。正しい起源は知りませんが、tweetaniumというサンプルがもとになっているようでした。

詳しい内容は、もう何人もの方が書かれているので、ここで書く必要はないでしょう。「Titanium Mobile」と「シングルコンテキスト」で検索してみてください。たくさんのページが出てきますから、その中から何ページかを読めば理解できるでしょう。いちおう簡単に説明すると、次のようになります。「Ti.include」で読み込みながらシングルコンテキストで作るものの、特別なグローバル・オブジェクトを用意し、それに付加する形でウィンドウや関数を作ります。付加するプログラムは内容で分割し、それぞれ違う名前の場所に加えることで、別々の名前空間を実現します。また、ぞれぞれのプログラム全体を、即時関数として作るのも大きな特徴です。なかなか賢い方法ですね。

 

さて、ここからが本題です。私が作った業務アプリも、同じ構造を採用しました。ただし、「シングルコンテキスト+即時関数」という考え方だけでは、アプリ全体をすっきりした構造にするは不十分でした。さらなる工夫が必要で、その工夫を紹介します。

今回の業務アプリの内容から、特別なグローバル・オブジェクトを「bb」としました。この「bb」オブジェクトにウィンドウや関数を追加するわけですが、どのように分割するかが悩むところです。iPadアプリですから、画面に表示した内容にタッチしながら、作業を進めることになります。何種類かの画面があって、画面ごとに機能が分割されています。画面ごとに分割するのは、理解しやすくて作りやすい考え方でしょう。また、扱うデータごとに分割するという考え方もあります。商品データ、顧客データ、販売データなどによる分割です。加えて、アラートや確認ダイアログのように、システム全体で使うプログラム機能もあるでしょう。この3つを全部入れて、3レベルに分割したら良いと考えました。整理すると、レベル1がプログラム機能、レベル2が扱うデータに関わる処理、レベル3が画面に関係する機能です。

せっかく3レベルに分けたのですから、それらを「bb」に付加するときのルールも用意しました。レベル1では、そのまま追加する。レベル2では、1文字の英字で追加する。レベル3になると、2文字の英字で追加するという具合です。具体的な例を書くと、次のようになります。

// 特別なグローバル・オブジェクト
var bb = {};

// レベル1
(function() {
    bb.alertSp = function(msg) { ... }
})();

// レベル2
(function() {
    bb.c = {};
    bb.c.getName = function(id) { ... }
})();

// レベル3
(function() {
    bb.pe = {};
    bb.pe.strNameTable = [];
})();

加える文字は、0〜2文字と少なくしてあります。文字数が少ないことで、記述しやすくなるのを狙っているからです。参照される頻度は、レベル1とレベル2が圧倒的に多いため、この2つをより少ない文字数に割り当てたわけです。

レベル2と3の文字には、それぞれ意味を持たせたほうが理解しやすくなります。レベル2では、扱うデータごとに異なる文字を割り当てるのですから、データ名の先頭文字が適しています。商品情報ならproductsのpを、顧客の情報ならcustomerのcを、販売情報ならsalesのsをという具合に。レベル3も同様で、画面に対応させた機能を英字2文字に略した言葉が適しています。商品情報の編集画面なら、英語での一般的な表現と言うことでedit productsを略したepとしたくなるでしょう。しかし、お薦めなのは、逆順のpeです(理由は後述)。こうすると多くの場合、1文字目が作業対象を、2文字目が作業の種類を表すことになります。それが理解できると、たった2文字なのに、何をどうしている画面なのか予想できるようになります。もし2文字では足りないと感じる場合は、このレベルを3文字にする方法もありまし、2文字と3文字の混在でも構いません。好きずきで選んでください。

とは言うものの、pやsは競争率が高く、どうしても衝突しがちです。そんな場合は、もっとも重要なものをpやsに割り当てて、それ以外は別な英字に割り当てます。また、環境設定(pref)のように、すべてのアプリで使いそうな機能は、あまり使いそうもない英字(zなど)を割り当てて、アプリが変わっても同じ文字を割り当てられるように決めておきたいものです。

 

分割方法と割り当て文字が決まったら、それに従ってJavaScriptを作成します。当然、対応文字ごとにファイルも分割します。その際に大事なのが、ファイル名です。ファイル名の先頭に、割り当てた文字を入れるのです。たとえば、商品情報を扱うJavaScriptなら、ファイル名を「products.js」とするのではなく「p_products.js」とします。同様にレベル3でも、「prod_edit.js」ではなく「pe_prod_edit.js」というように。このように分かりやすい内容なら先頭に付ける意味はありませんが、「z_pref.js」や「ez_pref_edit.js」のような名前なら価値が大きいでしょう。

これら以外にも、ヘルプのような特別な画面もあります。このような画面でも、グローバル・オブジェクトに加える以上は何らかの2文字が必要です。作業名として意味のある1文字を無理して作り出そうとすると、無駄に悩んでしまいます。ここは気軽に、helpを単純に2文字に略した「hp_help.js」で構わないと思います。何事も柔軟にですね。

割り当て文字をファイル名に入れると、ファイル名の一覧表にも表示されます。その結果、割り当てた文字を否応なく記憶されられてしまいます。頑張って覚えるという感覚ではありません。何の努力もせず覚えてしまいますから、「bb.h」とか「bb.ez」とかが探す必要もなくなります。だからこそ、ファイル名に含めることが大事なのです。また、ファイル名を見ただけで、使っていない文字並びも簡単に分かります。新しい2文字を追加する際にも、今まで使った2文字を調べる必要はありません。

さて、2文字の場合、ep(edit_prod)ではなくpe(prod_edit)にした理由の話です。作業対象を前に置くと、ファイル一覧にファイル名が並んだとき、同じ対象を扱うJavaScriptが連続して並びます。ファイル一覧が自動的に整理された感じに見え、管理しやすくなります。というわけで、作業対象を前に置いた割り当て方法を強くお薦めします。

base.js
c_customer.js
ce_customer_edit.js
cv_customer_view.js
cvp_customer_view_prod.js
hp_help.js
p_products.js
pe_prod_edit.js
pv_prod_view.js
z_pref.js
ze_pref_edit.js

この中で先頭にある「base.js」が、レベル1のJavaScriptです。先頭に並んでほしかったので「base」という言葉を選びました。本当は「a」で始まる言葉を使いたかったのですが、思い浮かびませんでした。「base」ならば「b」で始まるファイル名の中でも一番先頭に来そうですし、言葉の意味としても適していそうなので、とりあえず良いかなと思いました。

ここまででも、予想したより長くなってしまいました。続きは、別な投稿として書きます。

 

追記:言葉の間違いを直すため数文字変更しただけなのに、投稿した日付も更新され後ろに移動しました。bloggerのバグなのか仕様なのか、困ったものです。この追記で、また新しい日付に変わるのでしょうか。投稿順に並ばなくなりましたが、直すのが大変なので、このままにしましょうかね。

2012年3月16日金曜日

シングルコンテキストでの設計方法(3)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続き、第3回です。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、その中のレベル1の作り方について、少し詳しく書きます。

 

レベル1のJavaScriptに入れるのは、適用業務に無関係なプログラム上の機能です。代表的なものとして、共通のアラート、確認を求めるダイアログ、タイマー処理、簡単なアニメーションなどです。アニメーションというのは、ウィンドウのオープンやクローズの際のアニメーションだけではありません。文字入力するときにソフトキーボードが出て、画面の下側にある入力フィールドが見えなくなる問題への対応です。TextFieldなどをViewの上に置き、Viewごと上に移動する機能を実現します。

こういった機能も必要ですが、もっとも大事なのは、UI部品の記述を簡単にすることです。Titanium Mobileで作り始めて面倒に感じるのが、Ti.UI.createXXXとして記述するUI部品の生成処理でしょう。行数が増えるだけでなく、数多くのUI部品を付けるほど、ソースコードが長くなってしまいます。レベル1のJavaScriptとなる「base.js」に、UI部品を生成する関数を用意します。

// base.js (レベル1)
bb = {};
(function() {
    bb.createWinF = function(_title){
        return Ti.UI.createWindow({
            title:_title,
            backgroundColor:'#fff'
        });
    }
    bb.createLblF = function(_text, _fontSize, _textAlign, _height, _width, _top, _left){
        return Ti.UI.createLabel({
            text:_text,
            font:{fontSize:_fontSize},
            textAlign:_textAlign,
            height:_height,
            width:_width,
            top:_top,
            left:_left 
        });
     }
    bb.createBtnF = function(_title, _fontSize, _height, _width, _top, _left){
        return Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
    }
})();

レベル1の関数なので、用意したグローバル・オブジェクトの「bb」に、直接加えています。これらは、外から呼ぶことが可能なようにと「bb」に加えているわけです。逆に、base.js内でしか呼ばれない関数は、普通に「function func_name() { ... }」の形で書いて構いません。

こうした用意した関数をレベル3のJavaScriptで利用すると、次のような感じになります。

// pe_prod_edit.js (レベル3)
(function() {
    bb.pe = {};
    bb.pe.win = bb.createWinF('prod_edit');
    var lblTitle = bb.createLblF('商品情報編集', 36, 'center', 70, 'auto', 20, null);
    bb.pe.win.add(lblTitle);
    var lblMsg = bb.createLblF(' ', 18, 'left', 30, 500, 700, 200);
    bb.pe.win.add(lblMsg);
    var btnSave = bb.createBtnF('保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);
    bb.pe.win.add(btnSave);

    function saveDataF() { ... }
})();

普通に記述する方法よりも、格段に短くなったと感じたのではないでしょうか。UI部品の数が多くなるほど、その効果の大きさに感激します。無駄な記述が格段に減り、アプリ全体でのソースコードの量も大きく減ります。また、生成する処理の行が長くならないので、別に分ける必要もなく、生成内容と処理を近くに書けます。

これでもまだ不満、addすら何度も書きたくないという人には、生成する関数にaddも含めてしまう方法があります。

// base.js (レベル1) addを追加したもの
bb = {};
(function() {

    bb.createLblF = function(_view, _text, _fontSize, _textAlign, _height, _width, _top, _left){
        var lbl = Ti.UI.createLabel({
            text:_text,
            font:{fontSize:_fontSize},
            textAlign:_textAlign,
            height:_height,
            width:_width,
            top:_top,
            left:_left 
        });
        _view.add(lbl); // addします
        return lbl;
     }
    bb.createBtnF = function(_view, _title, _fontSize, _height, _width, _top, _left){
        var btn = Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
        _view.add(btn); // addします
        return btn;
    }
})();

これを利用する側のJavaScriptは、前よりもさらに短くなります。

// pe_prod_edit.js (レベル3) add対応版
(function() {
    bb.pe = {};
    bb.pe.win = bb.createWinF('prod_edit');
    var lblTitle = bb.createLblF(bb.pe.win, '商品情報編集', 36, 'center', 70, 'auto', 20, null);
    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF);

    function saveDataF() { ... }
})();
UI部品の標準設定のまま使う限り、addまで含めて1行で済んでしまいます。たいへん見やすいソースコードになりますね。私が作った業務アプリは、addを含まない生成関数を使いました。次に作るものは、addを含んだ生成関数を使おうと思っています。

addを含めた作り方は良いけど、addしない使い方も可能にしたいというワガママな要望がある場合は、次のように条件式を入れます。

// base.js (レベル1) addワガママ版
bb = {};
(function() {

    bb.createBtnF = function(_view, _title, _fontSize, _height, _width, _top, _left){
        var btn = Ti.UI.createButton({
            title:_title,
            font:{fontSize:_fontSize},
            height:_height,
            width:_width,
            top:_top,
            left:_left
        });
        if (_view) _view.add(btw); // 条件付きadd
        return btn;
    }
})();
// pe_prod_edit.js (レベル3) addワガママ版を利用
(function() {
    bb.pe = {};

    // 普通にaddするボタン生成
    var btn1 = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    // addしないボタン生成
    var btn2 = bb.createBtnF(null, '隠れ保存', 24, 40, 200, 100, 32);

})();

こんな感じで、いろいろとアレンジできますね。addしない生成にどんな使い道があるかは別にして。また、どんなアレンジを加えるかは、最終的に好みの問題でしょう。

 

生成関数を作る際の考慮点も、いくつか紹介します。生成関数で作るUI部品の設定は、アプリ内で一番標準的な形を選びます。そして、標準以外の設定で使いたいUI部品は、生成関数で作った直後に、好みの形に設定を変更します。こうすることで、アプリ全体の記述がもっとも少なくなるはずです。また、標準設定を変更するコードが加わることで、標準設定から何を変えたのか、すぐ理解できるようになります。このような関数を使わず、すべての設定が並んでいる記述だと、どの設定が他と違っているのか、非常に見付けにくいですから。

// pe_prod_edit.js (レベル3) 標準設定を変更する
(function() {

    var lblMsg = bb.createLblF(bb.pe.win, ' ', 18, 'left', 30, 500, 700, 200);
    lblMsg.color = '#888'; // 標準設定を変更

    var btnSave = bb.createBtnF(bb.pe.win, '保存', 24, 40, 200, 100, 32);
    btnSave.addEventListener('click', saveDataF); // これも変更の一つ

})();

アプリによっては、2種類の設定を多用することもあるでしょう。そんなときは、生成関数を1つに制限せず、2種類の設定を持った別々の生成関数を用意して使い分けます。2種類の設定を別々に持てるので、設定の変更もそれぞれ一箇所で済みます。もちろん、生成関数が1つの場合も、標準設定の変更が一箇所で済みますね。

 

引数の並び順も、できるだけ標準化したい点です。add版で考えると、add対象のビュー、ボタンなどの名前を最初に入れるでしょう。それ以降も、できる限り同じ値を入れて、先頭からの並び順を統一したくなります。候補としては、縦横の大きさと位置の4つの値が一番の候補でしょう。

しかし実際に試してみると、この4つの値は、もっとも右側に並べるのがベストです。UI部品を生成した行は、他の行よりも長くなります。すると、生成行の右側の値だけが、何もない空間に存在するような感じに見えるのです。その場所に4つの値を置くと、4つの値が見付けやすくなります。この4つの値は、UI部品の大きさと位置を調整するために何度も変更することが多く、少しでも見やすい形で作っておきたいのです。

というわけで、引数のお薦め並び順は、add対象のビュー、ボタンなどの名前、その他の設定値、縦横の大きさと位置の4値となります。最後の4つの値の並び順を統一することは、言うまでもありませんね。

Titaniumu Studioを使っていると、関数の引数を表示してくれるので、引数の並び順を覚えておく必要はありません。実際には、ラベルやボタンを生成した行をコピー&ペーストして、必要な箇所だけ引数を変更することが多いので、Studioの引数表示すら、めったに使いませんけどね。

 

以上のような形でレベル1のJavaScriptに、ウィンドウ、ビュー、タブビュー、ラベル、ボタン、テキストエリアなど、通常なら10種類ほどの生成関数を用意します。Ti.UI.createXXXとして作るものは、基本的にすべて対象となります。あとは、レベル3のJavaScriptでガンガン使うだけです。レベル3の記述が簡素化され、見やすいプログラムになるでしょう。

メリットを整理すると、次のようになります。

・UI部品を生成する長い記述がたった1箇所で済む(アプリ全体での記述量も減る)
・個々のUI部品生成は1行で済み、簡潔で読みやすくなる
・生成処理を別に分ける必要もなくなり、生成設定と処理を近くに置ける
・UI部品の大きさや表示位置の数値が右側に飛び出した感じで、変更するときに見付けやすい
・UI部品の標準設定を変更するとき、一箇所だけ直せばよい
・標準設定のまま使わないUI部品が、何を変えたのか見分けやすい

今回は、短いとはいえソースコードをいくつも貼り付けたので、全体が長くなってしまいました。レベル2とレベル3の作り方は、別な投稿に分けます。

2012年3月15日木曜日

シングルコンテキストでの設計方法(2)

Titanium Mobileを使ったアプリ開発において、シングルコンテキストで作る際の工夫の続きです。3つのレベルに分けてJavaScriptを作る、という話でした。今回は、レベル2(データを取り扱う処理)とレベル3(画面操作に関係する処理)の役割について書きますね。

 

オブジェクト指向でアプリを作る場合、MVCという考え方があります。M(モデル)、V(ビュー)、C(コントロール)という区分けで、アプリの機能を分割する方法です。このMとVとCを、レベル2とレベル3に当てはめて考えましょう。

基本となるデータを扱うレベル2は、Mに該当します。データを処理する機能のほとんどをレベル2に入れるからです。もう1つのレベル3は、VとCに該当します。画面を操作するタイプのアプリでは、VとCの分離が明確になりにくく、両方が混在した形になりがちです。VとCを完全に分離しなくてもアプリは作れますから、ここの分離に強く拘る必要はないでしょう。とくに処理速度が遅いモバイル機器のアプリではね。

 

一番大事なのは、MとVCの分離です。この分離さえ上手に作れれば、将来の機能変更に対して、変更箇所が少なくて済むアプリになります。すっきりした内部構造のアプリになるというわけです。この分離をキッチリ作るように意識しなければなりません。

正確に言うなら、レベル2がMに該当するのではなく、該当すると言えるようにレベル2を作るべきという話です。扱うデータをそのまま解放するのではなく、データの構造を隠蔽して外に出さずに、用意した関数を通してアクセスさせます。オブジェクト指向に慣れているなら、get、set、addなどのメソッドを通じてデータを扱う形を知っているでしょう。それと同じように、より使いやすい機能まで加えて作るということです。

ただし、モバイル機器で実行させる場合、処理能力の低さを考慮する必要もあります。さらに悪いことに、ネイティブのコンパイラ言語ではなく、JavaScriptで動かすわけですから、データ構造の隠蔽を徹底的に行うと、処理が遅いと感じることも起こりうるでしょう。実機で動かした経験をもとに、ある程度の妥協点を見付ける必要もあるようです。この辺は、アプリの処理内容に大きく依存するため、具体的なアプリで判断するしかありません。

Mの中で何種類ものデータを一緒に扱う場合は、データが相互に関係していることも多くなります。これもレベル2に含めましょう。たとえば、顧客情報と商品情報から、特定の顧客が購入した商品の一覧を表示するといった場合です。顧客IDを指定したら、商品IDの一覧を返すといった処理を、レベル2の上位層として追加します。JavaScriptを入れる場所は、bb.cでもbb.pでもなく、新しく1文字のbb.xなどを加えます。

モデルの種類が多いアプリの場合、レベル2の上位層まで加えると、レベル2を1文字で表すのが難しくなるでしょう。そんなときは、レベル2の上位層は2文字にして、レベル3を3文字以上にする手もあります。つまり、1文字はレベル2の基礎層でデータ単体での処理、2文字はレベル2の上位層で複数データ間の処理、3文字以上がレベル3とするわけです。

 

ここまでを読んで、「bb.model」や「bb.ui」と似た話ではないかと思った人がいるかもしれませんね。実は、ここで紹介しているレベル分けの表記方法は、「model」や「ui」の改良版とも解釈できる方法なのです。「model」はレベル2に、「ui」はレベル3に該当します。つまり、「model」と記述する代わりに、「p」や「c」と1文字にすることで実現しています。「bb.model.products」と書かなければならないところを、「bb.p」で済ませられるわけです。同様に、「ui」と記述する代わりに、「pe」や「cv」と2文字にすることで伝えています。「bb.ui.products_edit」と書かなければならないところを、「bb.pe」で済ませられます。

「model」や「ui」は、アプリ内の処理の区分けであって、ソースコード上に何度も何度も記述される必要はありません。ただでさえ長くなりがちな最近のソースコードが、ますます長くなってしまいます。それを防ぐ目的でも、このレベル分けの記述方法が役立ちます。短く簡潔なソースコードを目指すというわけです。

 

以上の話から、3つのレベルは次のように解釈できるでしょう。
・レベル1:基礎層
・レベル2:MVCのM層
・レベル3:MVCのVC層

キリがよいので、ここで一旦切ります。続きは、別投稿にて。