実験の開始
Bruce Tateは『Beyond Java』という著書の中で、あるJ2EEアプリケーションを何回かの週末を費やして完成させた後に、同じアプリケーションをRuby on Railsで構築してみたら1回の週末で済んでしまったというエピソードを紹介しています。私も本稿で同様の試みをしたいと思います。私はこの3年半、COBOLからJ2EEにリファクタリングされた大手レンタカー会社のWebベースのレンタカーアプリケーションに取り組んできたのですが、これをSeamで書くとどうなるかを考えてみます。なお、この挑戦にあたっては、48~72時間で終わらせるという目標時間を設定しました。
もちろん、このサンプルアプリケーションは、私のチームが長年取り組んできた実際の商用アプリケーションと完全に同じものではなく、スケールダウンしたバージョンです。とはいえ、使用している概念は、大抵の大規模な階層化J2EEアーキテクチャの基礎になっている一般的なものです。
このアーキテクチャでは、「5+1層」のパターンを使用しています。各層はそれぞれ一定の役割を担っています。階層化アーキテクチャでの依存関係の管理の仕方は、それぞれの層が直下の層にしか依存しないようにすることです。上方向の依存関係は有効ではありません。これは依存関係を管理し、大きな開発チームでうまく責任分担し、チーム全体で共通のやり方によって問題群を分析するための効果的な方法です。
各層はデータを転送する必要があり、そのデータはほとんど同じ形式になっています。そのため、私は「+1層」を作成し、すべての層のすべてのコンポーネントがそれに対して依存関係を持てるようにしました。この層には、実際のロジックが入っていないデータファイルが含まれています(図3を参照)。
図3から分かるように、最上層はプレゼンテーション層で、これはStrutsに基づいています。第2層はアプリケーション調整層で、ここにはセキュリティシステムとの統合が含まれています。セッションEJBがこの層とのやりとりを管理します。この層にはアプリケーション固有のロジックも含まれており、ここでビジネスドメインエンティティ間のやりとりを管理します。第3層はビジネス層で、エンタープライズ内の主要なドメインエンティティと一群の再使用可能なロジックと機能が含まれています。第4層はシステムの残りの部分と外部リソースの間の通信を管理します。この層はDAOに基づいており、各外部リソースは通信を管理するために1つ以上のDAOを持っています。第5層は必要な外部リソースです。アプリケーションの中には外部リソースとしてデータベースを1つだけ持っているものもあれば、18個もの異なる外部リソースを使用するものもあります。このアーキテクチャに基づくアプリケーションで、複数のコンポーネントとDAOを再使用するアプリケーションはいくつもあります。
データベース層は、Reservation、Location、Customer、CarClassという4つのテーブルから成ります。このデータベーススキーマはSeamアプリケーションで使われます。
アプリケーションフローはユーザーがCustomerレコードをReservationレコードに関連付けるところから始まります。ユーザーは次にピックアップとドロップオフのLocationレコードおよび両方の日時をReservationレコードに関連付けます。CarClassレコードはReservationレコードに関連付けられます。カークラスレートと予約期間に基づいて見積もり料金が計算され、Reservationテーブルに書き込まれます。
完成した予約エントリには、顧客参照、カークラス参照、ピックアップ(借り出し)とドロップオフ(返却)の場所、ピックアップの日時、ドロップオフの日時、そして最後に見積もり料金が含まれることになります。
Seamに取り組む週末
私はSeamを使った48~72時間の実験に取り組むにあたり、Hibernateコードジェネレータを使ってコードベースの最初の部分を作成しようと決めました。このツールはJBoss IDE JEMS製品の一部であり、Seamのスケルトンアプリケーションを作成するオプションがあります。個々のデータベーステーブルに対して次のコンポーネントが生成されます。
- テーブル内の行の実際のデータが含まれるHibernateオブジェクト
- Finderコンポーネント
- Editorコンポーネント
- Selectorコンポーネント
例えばLocationテーブルの場合は、このコードジェネレータによって2つのJSFページ、4つのクラス、2つのインターフェイスが生成されました(図4を参照)。参考までに、それぞれの生成コンポーネントについて簡単に紹介しておきます。
- Location.java -- Hibernateコンポーネントです。
- LocationFinder.java -- LocationFinderBeanのインターフェイスです。
- LocationFinderBean.java -- ファインダJSPページのためのページフローロジックとデータベース参照コードが含まれるセッションBeanです。
- LocationEditor.java -- LocationEditorBeanのインターフェイスです。
- LocationEditorBean.java -- 場所を作成または修正するためのロジックが含まれ、エディットJSPページのためのページフローを管理するセッションBeanです。
- LocationSelector.java -- 1つのインターフェイスと、そのインターフェイスを実装して複数の行をリストして選択できるようにするいくつかの静的インナークラスが含まれます。画面タイトル、ボタンラベル、テキストラベルのためのロジックも含まれます。
- editLocation.jspとfindLocation.jsp -- JSFページです。
- editLocation.jsp -- 特定のデータベーステーブル行やLocation.javaの特定のインスタンスを作成または更新するためのページです。
- findLocation.jsp -- 検索条件を設定したり、リストをページングしたり、リストから特定のデータベーステーブル行やLocation.javaの特定のインスタンスを選択したりするためのページです。
生成されたコンポーネントをコンパイルしてJBossにデプロイすれば、Location、CarClass、Customer、Reservationの各テーブルの検索、ページング、追加、削除、更新が可能になります。ここまでの作業には1時間もかかりませんでした。
しかし、私は何もかも気に入りませんでした。例えばReservationテーブルでは、ピックアップとドロップオフの場所の主キー、カークラス、顧客を予約に関連付けるためには、これらの情報を入力しなければなりませんでした。また、Reservationオブジェクトでは、これらのクラスを参照するときにオブジェクトではなく整数を使用していました。私は、この関係をもっとうまく管理するコードを生成したいと考えました。このようなコードが実際に生成されている例を見たことがあったので、それが可能なことは分かっていました。そこで、データベースで外部キーを使うことにしました。
CarClassへの外部キーを作成し、コードを生成してみたところ、その結果は満足のいくものでした。この段階で、予約作成テーブルにアタッチされたボタンをクリックすると、「findCarClass.jsp」ページが呼び出されるようになりました。このページからCarClass
オブジェクトを検索し、Reservation
オブジェクトに関連付けたいCarClassを選択することができます(図5を参照)。ここまでは1時間足らずで完了しました。
この時点では、外部キーをあと3つ(Customerテーブルに対するものが1つ、ピックアップとドロップオフの場所に対するものが2つ)追加すれば、作業の95%は完了すると思っていました。しかし、ここで最初の障害にぶつかりました。新たに外部キーを追加した後、生成されたコードはコンパイルされなかったのです。
エラーをよく調べてみると、ピックアップとドロップオフの場所に対するLocationテーブルへの外部キーを作成したので、場所と予約の間のやりとりを管理するいくつかのオブジェクトでメソッドが重複していることが分かりました。余分なメソッドをコメントアウトすれば簡単に修正できるエラーのように見えましたが、生成されたコードのフローをたどっていくのは手間がかかりました。4時間かかってデバッグを終えた後、コードは正常にコンパイルされたので、その夜はそこで仕事を切り上げました。
次の朝、コードをデプロイしましたが、予約に場所を追加するたびに例外が発生することに気付きました。昼食時になる頃、つまり4時間ほどあれこれ調査したのちに、事態を改善しようとした策が逆に事態を悪化させたことが明らかになりました。
そこで現在のアプローチを断念し、何かもっとうまいやり方を試すことにしました。まず、Locationテーブルから外部キーの1つを削除し、コードを生成し、コンパイルを行ってデプロイしました。10分もかからずに、CarClassとCustomer、さらに1つのLocationを統合したサイトを構築することができました。まず1つの場所だけを使って試してみるので、jspに手作業で機能を追加するにあたり、この時点ではバックエンドコードを変更しないようにしました。
この作業の結果、JSFコードは次のようになりました。
<div class="rvgResults"> <h2><h:outputText value="#{msg.Reservation_PickUplocation}"/></h2> <h:outputText value="#{msg.No} #{msg.Reservation_location}" rendered="#{ ''reservationEditor.instance.pickUpLocation'' == null}"/> <h:dataTable var="parent" value="#{ ''reservationEditor.instance.pickUpLocation''}" rendered="#{ ''reservationEditor.instance.pickUpLocation'' != null}" rowClasses="rvgRowOne,rvgRowTwo"> <h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg.Location_street}"/></f:facet> <h:outputText value="#{parent.street}"/> </h:column> <h:column> . . . <h:column> <f:facet name="header"> <h:outputText value="#{msg.Location_closetime}"/></f:facet> <h:outputText value="#{parent.closetime}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msg.Action}"/></f:facet> <h:commandButton action="#{ ''reservationEditor.pickUpLocation''}" value="#{msg.View} #{msg.Location}"/> </h:column> </h:dataTable> <span class="rvgPage"> <h:commandButton type="submit" value="#{msg.Select} #{msg.Location}" action="#{reservationEditor. ''selectPickUpLocation''}" /> </span> </div>
ピックアップの場所を表示するためにフロントエンドにフックを設けてあるので、ピックアップの場所を定義するためにReservationコンポーネントにメソッドを追加する必要がありました。「Reservation.java」を修正して、ピックアップとドロップオフの場所の変数が両方ともLocation
型になるようにしました。このうち一方の変数が、Locationテーブルの主キーを表すint
だったからです。さらに、これらの変数のゲッターメソッドとセッターメソッドをint
からLocation
型に修正しました。
「ReservationEditor.java」には、selectPickUpLocation()
、pickUpLocation()
、selectDropoffLocation()
、dropOffLocation()
の各メソッドを追加しました。これらのメソッドは本質的に元のselectLocation()
メソッドとlocation()
メソッドのコピーです。これらは名前こそ違うものの、実装は変わっていません。JSFコードを、これらの新しいメソッドが機能するように修正し、それから首尾よくデプロイしました。
ReservationEditor内の新しいメソッドはLocationSelectorインターフェイスとその静的インナークラスを使用します。ReservationとLocationの関係を統合するのに役立つインナークラスがあるので、そのインナークラスのコピーを2つ作成して名前を変更し、ピックアップとドロップオフの場所の選択に使用できるようにしました。さらに、インスタンスをインジェクトする際にこれらのクラスを識別できるように、アノテーション@Name
を使ってクラス名を指定しました。
@Stateless @Name("reservationPickUpLocationSelector") @LocalBinding(jndiBinding = "com.devx.res.example.ReservationPickUpLocationSelector") @JndiName("com.devx.res.example.ReservationPickUpLocationSelector") @Interceptors(SeamInterceptor.class) public static class ReservationPickUpLocationSelector implements LocationSelector { @Stateless @Name("reservationDropOffLocationSelector") @LocalBinding(jndiBinding = "com.devx.res.example.ReservationDropOffLocationSelector") @JndiName("com.devx.res.example.ReservationDropOffLocationSelector") @Interceptors(SeamInterceptor.class) public static class ReservationDropOffLocationSelector implements LocationSelector {
アノテーションの威力とSeamがそれらをどう使用しているかを実際に体験してみて、私はSeamを実に素晴らしい技術だと思うようになりました。
次に、「ReservationEditor.java」に追加したメソッドを修正して、先ほど作成した選択インナークラスでうまく機能するようにする必要がありました。
@Begin(join = true) public String selectPickUpLocation() { CONVERSATION.getContext().set("locationSelector", Component.getInstance("reservationLocationSelector", true)); return "selectLocation"; } @Begin(join = true) public String selectDropOffLocation() { CONVERSATION.getContext().set("locationSelector", Component.getInstance("reservationLocationSelector", true)); return "selectLocation"; }
@Begin(join = true) public String selectPickUpLocation() { CONVERSATION.getContext().set("locationSelector", Component.getInstance("reservationPickUpLocationSelector", true)); return "selectLocation"; } @Begin(join = true) public String selectDropOffLocation() { CONVERSATION.getContext().set("locationSelector", Component.getInstance("reservationDropOffLocationSelector", true)); return "selectLocation"; }
このような単純な変更により、「findLocation.jsp」ページとReservationEditorでlocationSelectorのまったく異なるインスタンスを使用することになります。この異なるインスタンスは、「editReservation.jsp」ページでどのボタンが選択されたかに応じて対話状態コンテキストに入れられます。ReservationEditorまたはそのクライアントである「createReservation.jsp」ページがlocationSelectorを参照すると、常に適切なインスタンスが取得されます。Selectorはボタンラベルやページタイトルなどを管理して、jspページが再使用されたときに、どういう理由でどの場所が選択されたかを識別できるようにします。
まだ解決すべき問題が1つ残っていました。「createReservation.jsp」ページでどのボタンが選択されたかによって、「selectLocation.jsp」の画面タイトルを変更する必要があります。ピックアップ、ドロップオフ、一般のいずれの場所であるかに応じて変わるようにする1行が必要でした。私は簡単な道を選び、2つのファイルの間で1行だけを変更して、2つのクラスを新たに生成しました。さらに、この2つの新しいクラスを使うように予約エディタを修正しました。具体的には、「ReservationEditor.java」を次のように変更しました。
@In(value="locationEditor",create = true) private transient LocationEditor locationEditor; public String pickUpLocation() { locationEditor.setNew(false); locationEditor.setInstance(instance.getPickUpLocation()); locationEditor.setDoneOutcome("editReservation"); return "editLocation"; } public String dropOffLocation() { locationEditor.setNew(false); locationEditor.setInstance(instance.getDropOffLocation()); locationEditor.setDoneOutcome("editReservation"); return "editLocation"; }
@In(value="pickUpLocationEditor",create = true) public String pickUpLocation(LocationEditor locationEditor) { locationEditor.setNew(false); locationEditor.setInstance(instance.getPickUpLocation()); locationEditor.setDoneOutcome("editReservation"); return "editLocation"; } @In(value="dropOffLocationEditor",create = true) public String dropOffLocation(LocationEditor locationEditor) { locationEditor.setNew(false); locationEditor.setInstance(instance.getDropOffLocation()); locationEditor.setDoneOutcome("editReservation"); return "editLocation"; }
Springにはメソッドにインジェクトする機能がありますが、使用は推奨されていません。私はSeamのこの機能を使って、ロケーションエディタの特定のインスタンスをメソッドシグニチャにインジェクトすることができました。変更前のコードではクラス変数へのインジェクションが見られ、変更後のコードではメソッドシグニチャ上の変数へのインジェクションが見られます。
ここまでSeamの実験にかけた時間は16時間になります。これだけの時間で、4つのデータベーステーブルのCRUD、検索、ページングが可能なアプリケーションを生成することができました。また、Reservation
オブジェクトから他の3つのオブジェクトへのオブジェクトレベルの参照を管理することもできるようになりました(3つのうちの1つが2つの参照を保持します)。Seamでの作業時間は16時間だったのに対し、元のアプリケーションでは同じ部分の開発に50時間かかっており、しかも機能は劣っています。