レベル: 中級 Federico Kereki, Systems Engineer, Freelance
2009年 7月 21日 GWT (Google Web Toolkit) を利用すると複雑な Web サイトを容易に開発することができます。ユーザビリティーを強化する特定のデザイン・パターン、そして Ajax (Asynchronous JavaScript and XML) と GWT とを組み合わせると、これらの技術と手法によって、軽快に動作するアプリケーションを実現することができます。その結果、通常の Web ページよりも従来のデスクトップ・プログラムに近いアプリケーションを作成することができます。
この記事のタイトルを JAB インデックス (JAB: Jargon (専門用語)、Acronyms (頭字語)、Buzzwords (流行語)) でランキング付けすれば高い順位になりますが、このタイトルではすべての用語
が一体になっています。この記事では、いくつかのプログラミング・パターンを検証します。こうしたパターンを GWT と Ajax と組み合わせると、より応答が高速で優れた Web エクスペリエンスを実現することができます。ところで、JAB インデックスとは何かを知らなくても心配する必要はありません。それを考えたのは私なのです。
ユーザビリティー
この記事のタイトルの等式に含まれる用語を右から順に見ていきましょう。最初はユーザビリティーです。通常の Web 指向の定義では、ユーザビリティーは使いやすいサイトを指します。つまり、わかりやすい画面レイアウトとワークフロー・ロジックを持ち、特別なトレーニングを必要としなくても利用できるサイトを指します。この記事の場合には応答時間に焦点を絞ります。応答性の悪いサイトは、当然、使い勝手も悪いですが、応答性が良いだけではユーザビリティーを実現することはできません。しかし私が提案するツールを使うことによって、サイトの他の側面に悪影響を与えずにスピードの違いを実感できるようになります。
Ajax
Ajax を利用すると、クライアントはバックグラウンドでサーバーからデータを取得することができ、より滑らかな動きの感覚をユーザーに与えることができます。Ajax を使って適切に設計された Web アプリケーションでは、(ローカルにインストールされる) 標準的なプログラムと変わらないルック・アンド・フィールを得ることができます。Ajax が登場する以前は、どんなデータを要求する場合でも、ただひたするサーバーの応答を待つしかありませんでした。Ajax を利用すると、ユーザーは作業を続けることができ、データは目に見えないうちに取得されます (実際には、必ず Ajax が必要なわけではありません。iframe を適切に使用すれば同じ効果を得ることができますが、この方法は Ajax よりも複雑です)。ユーザーを (ある程度の) 遅延から解放できるということは、より使いやすいアプリケーションにするための重要なステップであり、そうした点から全体の中に Ajax を含めることは適切なのです。
GWT (Google Web Toolkit)
GWT は完全にオープンソースの Java™ 開発フレームワークです。GWT では、Java 言語のみを使って Ajax アプリケーションを作成することができます。ここで少し注意が必要な点として、サーバー・サイドで Java コードを使うことは今や時代遅れになっていますが (アプレットを考えてみてください)、GWT の場合の Java とは、JavaScript コードにコンパイルされ、ユーザーのブラウザーで実行される Java コードのことを言っています。
GWT は Ajax を透過的に使います。クライアント・アプリケーションは、まるでクライアント・サイドのサーブレットであるかのようにサーバー・サイドのサーブレットを扱うことができます。これはつまり、クライアントとサーバーが (少し制限があるものの) クラスやコードを共有することができ、「よりシックな」クライアントを実現できるということです。GWT が登場する以前には、クライアントとサーバーとのやり取りのプログラミングは複雑であり、また当然ながらクライアントに Java コードを使うことはできませんでした。そのギャップが GWT によって埋められ、ユーザビリティーの高いサイトを容易に開発できるようになったのです。
パターン
では、パターンとは何でしょう。ソフトウェア・エンジニアリングにおけるパターンは、よく起きる問題に対して広範にわたって適用できるソリューションを表します。パターンは直接のソリューションではなく、むしろ「道筋」と言うべきものであり、実装の詳細はプログラマーに任されています。パターンによって、テスト済みで実証された仕組みが提供されるため、ソフトウェアを迅速に開発できるようになります。
パターンはアーキテクチャー上の概念として生まれたものであり、よくアーキテクチャーと比較されます。よく引用される例として、「窓」は日光を取り入れるためのソリューションですが、このソリューションによって窓の正確な形状や種類が規定されるわけではありません。ある 1 つの問題に対するパターンが 1 つである必要はなく (天窓や開放的なパティオ (訳注: パティオは、スペイン風の住宅にある中庭のこと) でも採光の問題を解決することができます)、また、どのパターンを適用するかは設計者次第です。この記事ではパフォーマンスに関するいくつかの問題を取り上げ、皆さん自身の Web サイトに使用できる、GWT に適したソリューションを示します。
テスト・データベース
 |
大規模なテスト・データベースを作成する
サーバー・サイドのサーブレットとクライアントのスピードをテストするためには、十分大規模なデータベースが必要でした。私は MaxMind が無料で提供している都市の表 (「参考文献」を参照) を使いました。それに ISO (International Organization for Standardization) 3166 の国コードの表を追加し、また ISO 3166-2 と FIPS (Federal Information Processing Standards) 10-4 の両方の地域コード表を追加しました。このようにした理由は、米国の都市のデータには ISO の数値コードではなく「AK」(アラスカの場合) のようなコードが使われるためです。必要なデータは CSV (カンマ区切り) ファイルとして提供され、これらを容易に MySQL テーブルにロードすることができました。
別の選択肢として、ランダム (あるいはほとんどランダムな) データを使うこともできましたが、都市の情報の方がわかりやすかったのです。他の選択肢については「参考文献」を参照してください。
|
|
私は、世界の国 (country)、地域 (region)、都市 (city) を含む単純なデータベース (リスト 1) を使いました。これを使った理由は、十分に大きなテーブルとオープンなデータベースが必要だったからです (囲み記事「大規模なテスト・データベースを作成する」を参照)。このデータベースは約 300 万件のレコードを提供できるため、私の目的には適していました。この記事で紹介する例を追うためには以下の点に注意してください。
- 国はコードで識別され (米国の場合は US など)、名前を持っています。
- 国には地域があります。地域は、その国内において一意に決まる (通常は数値による) コードで識別され、名前を持っています。
- 都市は、ある国の、ある地域内にあり、(純粋な ASCII による) 名前、その国の言語による名前 (英語以外の文字を含む場合もあります)、人口 (不明な場合は 0)、緯度、経度などの情報を持っています。都市名は、ある国の、ある地域内でのみ一意に決まります (例えば Springfield という都市名は米国内だけでも約 30 数ヶ所あります)。
リスト 1. データベースを作成するコード
CREATE DATABASE world
DEFAULT CHARACTER SET latin1
COLLATE latin1_general_ci;
USE world;
CREATE TABLE countries (
countryCode char(2) NOT NULL,
countryName varchar(50) NOT NULL,
PRIMARY KEY (countryCode)
KEY countryName (countryName)
);
CREATE TABLE regions (
countryCode char(2) NOT NULL,
regionCode char(2) NOT NULL,
regionName varchar(50) NOT NULL,
PRIMARY KEY (countryCode,regionCode),
KEY regionName (regionName)
);
CREATE TABLE cities (
countryCode char(2) NOT NULL,
cityName varchar(50) NOT NULL,
cityAccentedName varchar(50) NOT NULL,
regionCode char(2) NOT NULL,
population bigint(20) NOT NULL,
latitude float(10,7) NOT NULL,
longitude float(10,7) NOT NULL,
KEY `INDEX` (countryCode,regionCode,cityName),
KEY cityName (cityName),
KEY cityAccentedName (cityAccentedName)
);
|
ここでは GWT プロジェクトを作成しました (完全なソース・リストは「ダウンロード」を参照)。このプロジェクトには、シンプルなメニュー (図 1) と、2 つの同程度にシンプルな Web フォーム (Cities Creator と Cities Browser) があります。Cities Creator (図 2) は新しい都市をデータベースに追加するフォームであり、Cities Browser (図 3) は任意の国の任意の地域の都市名を取得するためのフォームです。コードは可能な限り単純に、余分なものを最小限にとどめて作成してありますが、それはパターンを示すことが目的であるためです。私は GWT バージョン 1.5.3 と MySQL バージョン 5.0 を使いました。また開発環境には OpenSUSE バージョン 10.3 Linux® 上で実行される Eclipse Ganymede を使いました。
図 1. サンプル・アプリケーションのメイン・メニュー (利用可能な両方のフォームを表示したもの)
図 2. データベースに新しい都市を追加することができる Cities Creator フォーム
図 3. 重複した都市名をユーザーが入力すると、Ajax によるバックグラウンド・チェックによって警告が表示され、該当フィールドが強調表示される様子
パターン: 事前検証
クライアント・サーバー・コンピューティングの基本は、サーバー・サイドですべてをチェックすることです (たとえサーバーを呼び出す前にデータが検証されているとしても、今まで正しかったデータが他のユーザーによる変更によって無効になっているかもしれません。元々は読めた記事が、たった今消されてしまったかもしれません)。しかし、クライアントとサーバーとの間をメッセージが往復するのを待ち、それからユーザーが単純なミスに気付く、という事態は避けたいものです。そのためのソリューションとしては、サーバー・サイドのチェック・ルーチンを Ajax によってバックグラウンドで呼び出します。そしてエラーがある場合には、ユーザーに警告し、不正なフィールドを強調表示し、等々を行います。
CitiesCreatorForm クラスと、このクラスの addDuplicateCityNameCheck メソッドを調べてみてください。ユーザーが既存の都市名を絶対に入力してはならないという前提の下、重複があるかどうかをチェックするサーバー・サイドの cityExists サービスがあることを考え、cityName テキスト・ボックスに ChangeListener を追加します。ユーザーが国、地域、都市名を入力したら、その時点で cityExists サービスを呼び出し、入力されたデータが既存データと重複していないかどうかをチェックします。
ただし、この記事のコードには小さな問題があります。非常にタイピングが速いユーザーが、既存データと重複する都市名を入力したものの、即座に誤りに気付き、その誤りを修正したと考えてみてください。するとそのユーザーには、(その時点では正しくなっている) フィールドが誤っているという警告が表示されてしまいます。そのための簡単な修正を CitiesCreatorForm2 クラスに加えています (リスト 2)。その修正とは、サービスのパラメーターを保存しておき、サーバーからの応答が得られたら、その保存してあるパラメーターと同じ値が相変わらずフォームの中にあるかどうかをチェックし、値が同じでない場合はエラーの処理をしない、というものです。
リスト 2. 事前検証パターンの擬似コード
create a new ChangeListener that will:
get the form field values needed for the check
if all fields are filled
save the form field values
call the server-side service to perform the check
on callback:
get the form field values again
if the current values match the saved values,
if there was an error,
highlight the fields
warn the user
otherwise
reset fields to normal
assign the created ChangeListener to all involved form fields
|
パターン: コードの共有
すべてのチェックをサーバー上で行う必要があるわけではなく、クライアント・サイドで多くのチェックを行えば行うほど、アプリケーションの動きは良くなります。昔ながらの Web 開発ツールでは、すべてのチェックを 2 度 (サーバーで 1 度、クライアントで 1 度) 行うようにコーディングされていますが、GWT では同じ Java コードをサーバーとクライアントの両方で使うことができます。ただし、サーバー・サイドはすべての Java コードを使用するかもしれませんが、クライアント・サイドのコードは JavaScript コードにコンパイルされるため、JavaScript 言語の制約を受けます。例えば、JavaScript コードではファイルを使うことができません。そのため、クライアント・サイドでは java.io を使うことができません。
コードを共有するパターンでは、クライアント・サイドのバージョンのクラスを実装し、それをサーバー用に拡張する必要があります (サーバー・サイドでは Java の機能をすべて利用することができます)。クライアント・サイドのコードはクライアント自身の限定的なオブジェクトしか扱えないため、サーバー・サイドには 2 つの特別なメソッドが必要です。そのメソッドとは、クライアント・サイドのオブジェクトを受信することができ、それを使ってサーバー・サイドのオブジェクトを作成できるコンストラクターと、サーバー・サイドのオブジェクトからクライアント・サイドのオブジェクトを生成できるメソッドの 2 つです。
この記事に含まれている ClientCityData クラスと ServerCityData クラスは、このパターンを示しています。クライアント・サイドのコードは、クライアントとサーバーとの間でオブジェクトを送受信できるように IsSerializable インターフェースを実装する必要があります。ServerCityData クラスはサーバーでのみ使用され、上述の特別な 2 つのメソッドを含んでいます。
パターン: キャッシング
ここまでの時点では、より高いパフォーマンスを実現するために GWT が役立ちましたが、GWT によってパフォーマンスが悪化する場合が 1 つあります。それはキャッシングが不可能な場合です。ブラウザーがページを要求する際には、ブラウザーは最初に自分自身のキャッシュの中を調べます。要求された結果がキャッシュにあることがわかると、ブラウザーはサーバーを呼び出すことはせず、キャッシュにあるデータを表示します (もちろん、キャッシュの中に入れられるには多くの条件を満たす必要がありますが、ここではそうしたことは重要ではありません)。問題は、GWT がサーブレットを呼び出す場合には、GWT はキャッシング不可能な Ajax プロシージャーによって RPC (Remote Procedure Call) を実行する点です。つまり同じデータをユーザーが繰り返し要求する場合であっても、ブラウザーは自分のキャッシュを使用しないため、毎回遅延が発生します (図 4)。
図 4. ある地域の都市名を取得しに行く Cities Browser の画面
あるページで、以前取得したのと同じ (不変の) 情報が必要な場合には、ローカル・キャッシュを設定することでパフォーマンスを改善することができます (リスト 3)。サーバーを呼び出す前に、必要なデータが既にロードされているかどうかを調べ、ロードされている場合には呼び出しをスキップします。もちろん、頻繁に変更される情報にキャッシュを使ってはいけません。時間の経過と共に情報が古くなる可能性がある場合には、タイムスタンプを追加し、古いデータを使わないようにします。
リスト 3. キャッシング・パターンの疑似コード
class_with_cache code:
define class attributes for the cache (a hash map, array, whatever)
set the cache to empty
whenever new data are asked for:
check if the asked data are already in the cache
if so,
get the data from the cache
perform whatever needs be done with it
otherwise,
display an appropriate "loading" message
call a server-side service to get the data
on callback:
put the data in the cache
perform whatever needs be done with it
|
この記事に含まれているコードには、このプロセスの例が 3 つあり、それらはすべて CitiesBrowser クラスのためのものです。最も簡単な例は、すべての国を表示するリスト・ボックス (CountryList クラスを参照) です。このリスト・ボックスの基本実装では、オブジェクトごとに (国のリストを取得するために) サーバーを呼び出していました。変更された CountryListWithCache クラスは、そのデータをクラス変数に保存し、そのデータをすべてのオブジェクトが共有できるようにしています。こうすると、最初にオブジェクトを作成する時しかサーバーへの呼び出しは行われません。
また、地域のリスト・ボックス (RegionList クラスを参照) も必要でしたが、このボックスの内容は現在の国に応じて変わるはずです。全世界には数千の地域があり、それらのすべてを取得することは現実的ではありません。私はキャッシュ (RegionListWithCache クラスを参照) を実装するために、ハッシュ・マップを使いました。つまり国が変更される (changeCountry メソッドを参照) ごとに、その国の地域を既に取得してあるかどうかを最初にチェックしたのです。
最後の例 (CitiesGrid と CitiesGridWithCache を参照) は、もう少し複雑です。地域の中にとても沢山の都市がある場合があるため、情報をページに分ける必要があります。私はクラスの属性としてハッシュ・マップを使いましたが、そのためには国、地域、開始ページを含むキーを作成する必要がありました。これは LoadCities メソッドに示してあります。
パターン: 先読み
サーバーからクライアントに大量のデータを送信する必要がある場合には、何らかの形でデータを塊にまとめる必要があります。ユーザーがどの情報を要求するのかが事前にわかるのであれば、Ajax メカニズムを使って先手を打ち、実際に必要になる前にデータを要求することができます。しかしユーザーが何を要求するのかは常に正しく推測できるわけではないため、誤って不必要なデータを取得してしまう可能性があります。この不必要なデータを取得するそのリスクと、先読みしない場合に確実に起こる遅延とのバランスを考慮する必要があります。
注意しなければならないのは、極端に走ってあらゆるものを先読みするといったことをしないようにすることです。そんなことをしてしまうと、むしろ結果は悪くなります。帯域幅が非常に制限されていたダイアルアップ・モデムの時代から、ブラウザーはクライアントとサーバーとの間の接続数を制限しています。その制限は HTTP (Hypertext Transfer Protocol) バージョン 1.1 の標準にも記載されています (「シングル・ユーザーのクライアントは、いかなるサーバーやプロキシーとの間にも 2 つを超える接続を保持『すべきではありません』」)。ホストに対していくつかのリクエストを送信しても、そのうちの 2 つが (並列に) 実行されるのみであり、それ以外のリクエストは、通常の遅延よりも長い時間、キューで待たされることになります。
CitiesBrowserWithCacheAndPreFetching には、この先読み機能を実装するために必要な変更が示されています。第 1 に、loadCities メソッドを変更し、ロードしたデータを必ずしも画面上に表示しないようにします (先読みを行う際にはロードしたデータを表示しません)。第 2 に、あるページを表示する (showCities メソッドを参照) 際には、必ず (論理的な推測により) 次のページを先読みします。ただし先読みしたページは表示しません。そして最後に、ユーザーが国と地域を選択したら、ユーザーの次の動作に備えて、最初の 2 ページを先読みします (リスト 4)。注意する点として、既に取得してあるページを先読みするようにコードが要求した場合には、実装されたロジックはサーバーへの不必要で冗長な呼び出しを避けるようにしています。
リスト 4. 先読みパターンの疑似コード
class_with_cache_and_pre-fetching code:
define class attributes for the cache (a hash map, array, whatever)
set the cache to empty
load_data method:
check if the asked data are already in the cache
if so,
get the data from the cache
perform whatever needs be done with it
otherwise,
display an appropriate "loading" message
call a server-side service to get the data
on callback:
put the data in the cache
if data were needed (as opposed to prefetched),
perform whatever needs be done with it
processing_data method:
call the load_data method to get that data
call the load_data method to get extra (prefetched) data
|
パターン: スレッド・シミュレーション
プロセッサー負荷の重いタスクを考えてみてください (例えば大量の XML の処理や大量のデータの表示など)。このプロセスにかかる時間が長すぎると、ユーザーにはメッセージが表示されます。Firefox の場合のメッセージは「このページのスクリプトがビジーか、あるいは応答しなくなっています。このスクリプトを今すぐ停止することも、このまま継続し、このスクリプトが完了するかどうかを確認することもできます。」という内容のものであり、Windows® Internet Explorer® での同様のメッセージは「このスクリプトの実行を停止しますか? このページのスクリプトが、Internet Explorerの実行速度を遅くしています。スクリプトを実行し続けると、コンピュータが応答しなくなる可能性があります。」という内容のものです。さらに悪いことに、ユーザーがその警告に従ってスクリプトを停止させると、実際にはクライアント・サイドのプログラムを停止させてしまうことになります。
このエラーは通常はスレッドを使って解決しますが、GWT ではスレッドを使うことができません。JavaScript 言語には実行スレッドが 1 つしかないからです。そのため、スレッド化されたコードをコンパイルしても適切に動作しません。Ajax を使えばサーバー・サイドのプロセスから抜け出すことはできますが、クライアントには役に立ちません。幸いなことに、このためのパターンが 2 つあります。そのパターンを適用すると、全ページの都市を表示することができます。
タイマー・ベースのソリューション
GWT には schedule() メソッドを持つ Timer クラスが用意されています (schedule() メソッドは JavaScript 言語の setTimeout() メソッドと似ています)。考え方としては、ちょっとした処理によって値を保存し、そのプロセスを後で (タイムアウト後に) 継続できるようにし、それまでの間プロセッサーを解放します (リスト 5)。そして、このプロセスを継続する必要があるかどうかをチェックします。つまりユーザーは次のページまたは前のページを表示しようと決めたかもしれず、その場合にはプロセスの中断前に表示されていたページのデータを表示する必要はありません。
リスト 5. タイマーによるスレッド・シミュレーション
define a class that extends Timer:
define attributes so it can save its parameters
define attributes so it can save local variables from run to run
define attributes so it can save form field values
on construction:
save the received parameters
initialize local variables for the process
save the current form field values
display a "loading" message
run() method:
if the current form field values match the saved values:
execute some process, updating the local variables
if there's still more work to be done
schedule another process in a short while
whenever you want to simulate a thread with a timed method:
create an object of the new class above, with appropriate parameters
execute its run() method
|
CitiesGridWithCacheAndPreFetchingAndTimer クラスには、このパターンが示されています。プライベート・クラス TimedCitiesDisplay は Timer クラスを継承しています。TimedCitiesDisplay クラスが作成されると、このクラスは都市のリストを受信し、イテレーターを初期化して、その都市リストに対して繰り返し処理を行います。またこのクラスは、現在の国、地域、そしてページを保存し、後でそのプロセスを継続する必要があるかどうかをチェックします。run() メソッドはいくつかの都市を調べます。さらにいくつかの都市が残っている場合には、run() メソッドは今後の実行をスケジューリングし、後でその実行が継続される場合には、停止された場所から処理が再開されるようにします (図 5)。
図 5. バックグラウンドでロードされている都市が処理の途中で表示されている様子 (一部の都市はまだロードが完了していません)
このソリューションをスムーズに動作させるためには、各ステップで行われる作業の最大量と、各ステップの間に許容される時間間隔とを検討する必要があります。短いステップが大量にある場合には応答性の高いマシンになるかもしれませんが、その代わりにすべてのデータを取得する場合には待ち時間が長くなります。一方で、各ステップが長いと「スクリプトがビジー」というメッセージが表示されることになり、これも適切ではありません。実験しない限り、最適な妥協点はわかりません。
GWT 独自の遅延コマンド
遅延コマンド (deferred command) は GWT 特有の機能であり、より適切なソリューションとなる場合があります。遅延コマンドはキューに入れられ、プロセッサーに空きがある時に実行されます (リスト 6)。ソリューションとしては、プロセスを小さなステップに分け、ただし Timer クラスの代わりに遅延コマンドを使います。すると、次のステップの計算をいつ実行するのかを GWT が判断します。
リスト 6. 遅延コマンドを使ったスレッド・シミュレーション
define a class that extends IncrementalCommand:
define attributes so it can save its parameters
define attributes so it can save local variables from run to run
define attributes so it can save form field values
on construction:
save the received parameters
initialize local variables for the process
save the current form field values
display a "loading" message
execute() method:
if the current form field values match the saved values:
execute some process, updating the local variables
if there's still more work to be done
return true, so it will run again shortly afterwards
otherwise,
return false (the job is done)
otherwise,
return false (situation changed)
whenever you want to simulate a thread with a deferred command:
create an object of the new class above, with appropriate parameters
use the addCommand() to add your new object to the processing queue
|
CitiesGridWithCacheAndPreFetchingAndDeferredCommands クラスには、このパターンが示されています。タイマーによるソリューションとの主な違いは、コマンドがキューに入れられ、プロセスをキューに戻して実行を継続する必要がある場合には execute() メソッドが True を返す点です。
このソリューションはタイマーによるパターンよりも柔軟です。マシンの応答が悪くなるようなことをユーザーがしなければフルスピードで処理が行われるからです。ただし、このパターンを過度に使うことは避ける必要があります。
まとめ
この記事では、バックグラウンドで Ajax を使用することで GWT アプリケーションの動作を速くすることができるデザイン・パターンについて説明しました。またこの記事では、JavaScript の一般的な制約 (スレッドが少ないこと、など) や、サーバーからのデータを要求する際の通常の時間遅延、といった問題への対策をいくつか紹介しました (先読み、キャッシング、クライアントでローカルに行う事前検証など)。GWT、Ajax、そしていくつかのパターンを使用することで、アプリケーションを高速化することができ、より応答性が良い Web サイトをユーザーに提供することでユーザビリティーを高めることができます。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Full source code for this article | full_source_code.zip | 24KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Federico Kereki はウルグアイ人のシステム・エンジニアであり、システム開発やコンサルティング、大学での教育に 20 年を超える経験があります。現在はおなじみの頭字語、SOA、GWT、Ajax、PHP、そしてもちろん FLOSS を渾然一体に扱いながら業務を行っています。 |
記事の評価
|