2008-05-22

なぜ Flex向けのサービスをRubyで記述することにこだわるのか

最近のエントリを見てわかるように、しばらく BlazeDS, Spring, JRubyのソースを追いかけたり書き換えたりしていた。なぜそうまでして Flexから JRubyを使いたいのか書いておくことにする。

video
※音が出るので注意※

この動画は「とあるサービスにおいてデータベースからメンバー検索を行う」機能をRIAとして実装することを想定した画面のサンプル(と思っていただきたい。実際に業務でこんなものを作ったわけではないので悪しからず)の作動する様子なのだが、このアプリケーション、実はモックアップである。サーバと通信はするものの、サーバサイドではデータベースへ問い合わせをせずにプリセットの固定データからレコードを検索して返すようになっている。

顧客を交えてシステムの画面設計を進める際は、このような「実際に動作する」モックアップを素早く製作して提示し、食い違いがあればすぐに修正する(場合によっては、捨てて作り直す)というプロセスを繰り返すことで安心感を得るとともに食い違いのリスクを軽減することが出来る。

このモックアップの特徴は、実際にサーバと通信を行うことである。すなわち、モックアップを製作する段階で画面デザインのみならず システムの入出力仕様(ユーザはシステムに何を入力して何を得るか)まで決まるということだ。単に画面デザインを提示して議論するよりも、システムの辻褄が合わない所やお互いの理解や考えが足りていない部分を早期に洗い出すことが出来る分そのほうが合理的だし、極端な話をすると全ての入出力について仕様が決まればそれより後ろ側のことはデータベース設計も含めて他の者へ任せてしまうことすら出来るので、設計の分業も可能になる。効果的な分業により(理論上は)開発組織全体のスループットを上げられる。

さて、アジャイル式に則り、実際に顧客の目の前でモックアップを製作したり修正する課程を想像してみよう。

まずは GUIのデザインである。テキストボックス、データグリッド、ボタンを配置していく様は、顧客の目から見てもわかりやすい作業であるため、彼らの興味を引き一緒に考えることが出来るだろう。Adobeの手先ではないが、幸いにして Flex Builderならば、VisualBasicやDelphiばりの早さで GUIをデザインできるため比較的このようなやり方に現実性がある。また、サーバと通信する部分はActionScriptによるコーディングが必要だが、ActionScriptからサーバ側の Javaメソッドを呼び出すには単純な関数呼び出しのように少ないコードを書くだけで済む。

いっぽうサーバ側のコードはどうだろうか。データベースへアクセスしないモックアップとはいえ、これを長々と書いていると顧客が寝てしまうため迅速に書き下ろさなくてはならない。

そう、きわめて迅速にである。

・・・つまりここで Rubyが登場する。

UI 側に公開するインターフェイス


このインターフェイスがすなわち、動画でお見せした画面からの入出力(呼び出し)仕様となる。
BlazeDSの仲介により、公開メソッドは ActionScript3から直接呼び出せる。従来のWebアプリケーションでやったように、つまらないコントローラクラスやForm Beanを作る必要は無い。

返値の型がMapだったりするのは仕様としてどうかという声もあると思うが、そのくらいなら自分のオフィスに戻ってから直しても遅くないだろう。
package net.stbbs.jrubytest;

import java.util.Collection;
import java.util.Map;

public interface MemberService {
/**
* メンバーを検索し、結果をコレクションで返す
* @param nameOrProperty 名前又は属性名に対して有効な検索語。
* nullの場合全てのレコードがマッチする
* @return 検索にマッチしたレコードのコレクション
*/

public Collection<Map> searchMember(String nameOrProperty);
}

公開インターフェイスが決まったら、それを実装する。Spring Frameworkのスクリプト言語サポートを用いれば、Javaで書いたインターフェイスを Rubyで実装することが可能だ。

rubyで実装した場合

class MemberService 
def searchMember(nameOrProperty)
[
{:id=>'konata',
:name=>'泉 こなた',:birthday=>'05月28日',:hometown=>'埼玉県',
:bloodtype=>'A',:breasts_rank=>'極小',:height=>142,
:properties=>{'オタク'=>5,'胸'=>1,'アホ毛'=>5,'運動神経'=>5,'需要'=>4}},
{:id=>'tsukasa',
:name=>'柊 つかさ',:birthday=>'07月07日',:hometown=>'埼玉県',
:bloodtype=>'B',:breasts_rank=>'小',:height=>158,
:properties=>{'バルサミコ酢'=>5,'巫女'=>3,'ドジっ娘'=>4,
'どんだけ〜'=>5,'妹属性'=>4}},
{:id=>'kagami',
:name=>'柊 かがみ',:birthday=>'07月07日',:hometown=>'埼玉県',
:bloodtype=>'B',:breasts_rank=>'中',:height=>159,
:properties=>{'ツインテール'=>5,'お姉ちゃん'=>3,'ツンデレ'=>5,
'お菓子好き'=>4,'ツリ目'=>5}},
{:id=>'miyuki',
:name=>'高良 みゆき',:birthday=>'10月25日',:hometown=>'東京都',
:bloodtype=>'O',:breasts_rank=>'巨',:height=>166,
:properties=>{'天然'=>5,'巨乳'=>5,'眼鏡っ娘'=>5,'委員長'=>5,'虫歯'=>5}}
].find_all {|m|
nameOrProperty == nil \
|| m[:name].include?(nameOrProperty) \
|| m[:properties].any?{|k,v| k.include?(nameOrProperty) }
}
end
end
顧客が眠そうな顔をしていたら、もっと少ないレコード数でやめておくべきかもしれないし、逆に根気よく付き合ってもらえそうなら ActiveRecordを使って実際にデータベースへ入出力する所まで書けるかもしれない。みさおとあやのも入れてあげてと言われたら入れてさしあげるべきだろう。

さて、これを Javaで実装したらどうなるだろう?

Javaで実装した場合

package net.stbbs.jrubytest;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class MemberServiceImpl implements MemberService {

protected Map createRecord(
String id, String name, String birthday,
String hometown, String bloodtype,
String breasts_rank, int height, Map properties)
{
Map m = new HashMap();
m.put("id", id);
m.put("name", name);
m.put("birthday", birthday);
m.put("hometown", hometown);
m.put("bloodtype", bloodtype);
m.put("breasts_rank", breasts_rank);
m.put("height", height);
m.put("properties", properties);
return m;
}

public Collection<Map> searchMember(String nameOrProperty) {
Collection<Map> results = new ArrayList<Map>();
Map p = new HashMap();
p.put("オタク", 5);
p.put("胸", 1);
p.put("アホ毛", 5);
p.put("運動神経", 5);
p.put("需要", 4);
results.add(createRecord(
"konata", "泉 こなた", "05月28日", "埼玉県",
"A","極小",142, p ));

p = new HashMap();
p.put("バルサミコ酢", 5);
p.put("巫女", 3);
p.put("ドジっ娘", 4);
p.put("どんだけ〜", 5);
p.put("妹属性", 4);
results.add(createRecord(
"tsukasa", "柊 つかさ", "07月07日", "埼玉県",
"B", "小", 158, p ));

p = new HashMap();
p.put("ツインテール", 5);
p.put("お姉ちゃん", 3);
p.put("ツンデレ", 5);
p.put("お菓子好き", 4);
p.put("ツリ目", 5);
results.add(createRecord(
"kagami", "柊 かがみ", "07月07日", "埼玉県",
"B", "中", 159, p ));

p = new HashMap();
p.put("天然", 5);
p.put("巨乳", 5);
p.put("眼鏡っ娘", 5);
p.put("委員長", 5);
p.put("虫歯", 5);
results.add(createRecord(
"miyuki", "高良 みゆき", "10月25日", "東京都",
"O", "巨", 166, p ));

if (nameOrProperty == null || "".equals(nameOrProperty)) return results;

Collection<Map> searchResults = new ArrayList<Map>();
for (Map m:results) {
boolean hit = false;
if ( ((String)m.get("name")).indexOf(nameOrProperty) >= 0) {
hit = true;
} else {
for (Object property:((Map)m.get("properties")).keySet()) {
if ( ((String)property).indexOf(nameOrProperty) >= 0) {
hit = true;
break;
}
}
}
if (hit) searchResults.add(m);

}
return searchResults;
}
}

・・・・・・・・・・・・・・寝る。確実に寝る。
これを書いている途中で、1レコードの生成処理を別メソッドに分けるというリファクタリングまでしている。
下手をすると顧客はその場を中座して煙草を吸いに行ったまま帰ってこないかもしれない。

Ruby版のほうは、ほぼ本質的な内容(羅列したい固定レコードと簡単なマッチング条件)だけで占められているため非コーダーにとっても何となく分かるようなものだが、Java版はいかにもプログラムコードで、意味不明な単語の出現頻度があまりにも多い。

誤解しないでいただきたいのは、Rubyの方が高い記述性のために優れており全てのシーンにおいて Rubyを選択するべきである、と言いたいわけではないことだ。

はっきり言ってしまえば、よほどユニットテストのノウハウに自信があるのでなければ並のプログラマに Rubyを使わせるなどということはできない。静的言語のように制約を受けないから間違い放題だし、そもそも彼らは Rubyの力を使いこなすことが出来ないため Javaで書かせた場合と比べて記述が少なくもならない。つまり、良いことがまるでない。

設計の局面で Rubyを使う理由としてここで語りたかったことをもう少し具体的に示すならば、アーキテクトが顧客との緊密な打ち合わせの上でモックアップを早く作ることによって設計を早期に固めてしまいさえすれば、後は並のプログラマに Javaで手堅く実際のロジックを書かせることで効果的に分業が出来るのではないかという話である。引数と返り値が決まっていて中身を実装するだけなら誰でも出来るのだから、アーキテクトはそんな誰でも出来る仕事を早く作って早く渡すのがその務めだろう(彼らの書いたコードの品質を制御する方法はまた別の問題だし、もしプロジェクトの規模が大きくドメインモデルのアーキテクチャを取ろうとするならばアーキテクトの仕事はもっと多くなるが)。

少し無理な当てはめ方をするならば、アジャイル的方法と旧来のウォーターフォール的方法をハイブリッドにしたものと考えられなくもない。

そのスタイルがうまく発展すれば、アーキテクトはギークなりスーパーコーダーとしての素養を発揮しつつも、外国の並以上かつ安価なプログラマを人材の補完に充てるという方法につなげることが出来るかもしれない。オフショア開発で起こる問題の原因は常に提示する仕様の曖昧さにあると考えられるが、仕様書の付録として「RPC込みで動作するモックアップ」があれば設計意図の伝わりやすさは格段に高いはずだし、設計自体の論理的誤りも少ないはずなので、きっとオフショアリスクはいくぶんかそこで軽減出来る。

ついでだから Flexのソースも載せておく


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
backgroundColor=
"white" verticalAlign="middle" horizontalAlign="center">
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable] private var properties:ArrayCollection;
private function doSearch():void
{
memberService.searchMember(
nameOrProperty.text !=
""? nameOrProperty.text : null);
}
]]>

</mx:Script>
<mx:RemoteObject id="memberService" destination="memberService"
endpoint=
"./messagebroker/amf"/>
<mx:VBox>
<mx:VBox width="100%" height="200"
backgroundImage=
"@Embed('assets/members.jpg')"
verticalAlign=
"middle" horizontalAlign="center">
<mx:HBox verticalAlign="middle" horizontalAlign="center">
<mx:Label text="名前又は属性で検索" color="#000000" fontSize="18"
fontWeight=
"bold"/>
<mx:TextInput id="nameOrProperty" fontSize="18" enter="doSearch()"/>
<mx:Button label="検索実行" fontSize="18" color="#000000" click="doSearch()"/>
</mx:HBox>
<mx:Label color="#000000" fontSize="16"
text=
"※空欄で検索すると全てのレコードが表示されます"/>
</mx:VBox>
<mx:HBox verticalAlign="middle">
<mx:VBox>
<mx:Label text="検索結果" fontSize="18"/>
<mx:DataGrid id="results" height="250"
dataProvider=
"{memberService.searchMember.lastResult}">
<mx:columns>
<mx:DataGridColumn headerText="写真" dataField="id" width="50">
<mx:itemRenderer>
<mx:Component>
<mx:Image source="images/{data.id}.png" height="48"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
<mx:DataGridColumn headerText="名前" dataField="name" width="90"/>
<mx:DataGridColumn headerText="誕生日" dataField="birthday" width="70"/>
<mx:DataGridColumn headerText="出身地" dataField="hometown" width="90"/>
<mx:DataGridColumn headerText="血液型" dataField="bloodtype" width="50"/>
<mx:DataGridColumn headerText="胸ランク" dataField="breasts_rank" width="50"/>
<mx:DataGridColumn headerText="身長" dataField="height" width="60"/>
</mx:columns>

<mx:change>
<![CDATA[
if (results.selectedItem == null) {
properties = null;
return;
}
var snd:Sound = new Sound();
var req:URLRequest =
new URLRequest(
"sounds/" + results.selectedItem.id + ".mp3");
snd.load(req);

properties = new ArrayCollection();
var p:Object = results.selectedItem.properties;
for(var i:Object in p) {
properties.addItem({key:i,value:p[i]});
}

snd.play();
]]>

</mx:change>
<mx:valueCommit>
<![CDATA[
properties = null;
]]>

</mx:valueCommit>
</mx:DataGrid>
</mx:VBox>
<mx:Image source="@Embed('assets/rightarrow.png')"/>
<mx:VBox>
<mx:Label text="属性一覧" fontSize="18"/>
<mx:DataGrid dataProvider="{properties}" selectable="false">
<mx:columns>
<mx:DataGridColumn headerText="属性名" dataField="key" width="80"/>
<mx:DataGridColumn headerText="ランク" dataField="value" width="50"/>
</mx:columns>
</mx:DataGrid>
</mx:VBox>
</mx:HBox>
</mx:VBox>
</mx:Application>

関連記事



Adobe BlazeDSを自分のWebアプリケーションに組み込む

Adobe BlazeDS(オープンソース)を使ってActionScript3からサーバ側の Javaメソッドをコールできるようにする設定方法

An alternative of BlazeDS - Rails and WebORB

ActionScript3からサーバ側の Rubyメソッドをコールできるようにする Railsプラグインの使い方

More alternative of BlazeDS - PHP and WebORB

ActionScript3からサーバ側の PHPメソッドをコールできるようにする PHPライブラリの使い方

Springのスクリプト言語サポートを使って BlazeDSと JRubyを統合する

実際に BlazeDS経由で ActionScript3から呼び出せるように Rubyスクリプトを配置する方法

ラベル: ,

0 件のコメント:

コメントを投稿

<< ホーム