japan.internet.comThe Internet & IT Network
Twitter
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年11月7日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
任天堂が、大画面の「ニンテンドーDSi LL」を発表。欲しいと思いますか?
欲しい
欲しいと思わない
他のDS製品を持っているが欲しい
他のDS製品を持っているのでいらない
投票締切 11/9 12:00
デベロッパー コラム2006年8月8日 12:00
DevX
DevX japan.internet.com 編集部(japan.internet.com)メールホームrss
米国 WebMediaBrands が運営する、
企業向けアプリケーションの開発者向けの技術情報/サービスサイト。

SeamでJavaプロジェクト開発を大幅に効率化する

海外海外internet.com発の記事

はじめに

 先日、ある友人が電話をかけてきて、今回のAtlanta JUGミーティングはJBossの新製品「Seam」の情報を得ようとする人々で大変な盛況だったと語りました。私はコンピュータの前に座ってSeamのことを20分ほど調べ、とても好印象を持ちました。SeamはJava EE 5(J2EE 3)の軽量な標準規格をベースにしており、新しいエンティティBean仕様、JSF、アノテーション、インターセプタ、セッションBeanといった技術を使用しています。Springと同様に制御の反転(Inversion of Control)を使用しますが、Springとは異なり、ステートフルなオブジェクトの注入を許可します。エンタープライズJava開発者が7年間もコツコツとやってきたデータ移動とフレームワーク/API操作の作業の多くがSeamによって解消されるのです。

 ユースケースとユーザーストーリーは対話的(conversational)なやり方で要件を記録しますが、私の知る限り、Seamは対話的なやり方でコーディング作業を助ける最初の製品です。Seamを利用すると、アプリケーションのページレベルおよびBPMレベルのやりとりが、ファーストクラスのエンティティになり得ます。ユースケースとユーザーストーリーが実際にコードのモデルになるのです。

 私は、もし自分の開発チームでSeamを使用していたらこの3年半にどれだけ多くのコードを節約できただろうかと考えてみて、Seamの有効性を実感しました。おそらく40〜60%のコードが不要になったと思われます。

 その後、実際にSeamを試してみて、コーディングが減って開発が楽になるという話が本当なのだと確認できました。そのことを、本稿で証明したいと思います。

編集者注
 Mark SmithはValtech Technologiesの取締役です。同社はSeamの開発元であるJBossとコンサルティングに関して協力関係を結んでいます。

Seamの概要

 Seamのメリットはいろいろありますが、私の考える重要な特徴は次の3つです。

  • Java EE 5標準に基づいている
  • 宣言的状態モデル
  • 制御のサブバージョン(バイジェクションともいう)

 SeamはJava EE 5仕様の概念をさらに推進し、軽量な開発とプログラマにとっての使いやすさを念頭に置いて設計されています。SeamはJSFとEJBのための強力な統合フレームワークを備えており、普通ならこれらの製品を統合するために使われるグルー(接着剤)コードが不要になります。開発者はオブジェクトからフォームやJSPへのデータ移動に時間を費やす必要がなく、ビジネスの価値を生み出すことに専念できます。

 Seamによる統合レベルでのコスト削減の効果は、Seamの基盤であるHibernate/EJB 3エンティティBeanアプローチに最もよく現れています(表1を参照)。Seamにより、JDBC呼び出しからオブジェクトへのデータ移動およびその逆のデータ移動に必要なロジックが不要になります。また、クラスレベルの複雑な関係管理はSeamが担当してくれるので、開発者は、過去のJDBCアプリケーションでこれらの問題を処理するために必要とされたコードや外部キーの詳細を気にせずに済みます。Hibernateはこれらの概念をEJB 3.0仕様と共に導入しましたが、これらはJava標準として採用されています(このモデルの詳細については、「Simplify Java Object Persistence with Hibernate」を参照してください)。

表1 JDBC呼び出しとEJB 3/Hibernateアプローチでの行数の比較
JDBCによる顧客統合管理クラス行数
AbstractJdbcDao.java169
CustomersJdbcDao.java301
ICustomerVO.java89
CustomerVo.java177
CustomerComponent.java71
合計807
Hibernateによる顧客管理統合クラス行数
Customer.java179
CustomerFinder.java(クエリメソッドのみ、フロントエンド制御ロジックなし)66
合計245

 Seamの宣言的状態モデル(図1を参照)では、状態を持つオブジェクトを管理するためのコンテキストを宣言できます。コンテキストになり得るものは、アプリケーション、セッション、対話(conversation)、ページ、イベント、BPMなどです。状態のコンテキストを宣言すると、Seamは必要な間はその状態を保持し、不要になると破棄します。状態管理によって、第2レベルのデータベースキャッシュ、ステートフルセッションBean、またはhttpセッションに関係する多数のバグとパフォーマンス上の問題が解決されます。また、APIの使用に関係するグルーコードも不要になります。

図1 Seamのモデル:Seamにより、JSFとJava EE 5仕様の他の部分の間のグルーコードの多くが不要になる。Seamは状態のコンテキスト管理を複雑なJava EE 5 Webアプリケーションに取り入れている(出典: JBOSS)
図1 Seamのモデル:Seamにより、JSFとJava EE 5仕様の他の部分の間のグルーコードの多くが不要になる。Seamは状態のコンテキスト管理を複雑なJava EE 5 Webアプリケーションに取り入れている(出典: JBOSS)

 制御の反転(IoC)はSpringで広く知られるようになった概念であり、しばしばハリウッドのプロデューサー的な「Don’t call me, I’ll call you.(電話をかけてくるな。必要があれば僕からかけるから)」という言葉で表現されます。IoCの概念では、ある種の処理をアプリケーションコードから分離してフレームワークまたはコンテナに移し、ある種のAPIの管理をアプリケーションコードからコンテナに移します。依存性注入(dependency injection)はIoCの一種であり、指定されたオブジェクトの作成または管理をIoCコンテナに移します。以前の手法ではオブジェクトのフェッチとAPIを処理するコードを書かなければなりませんでしたが、依存性注入では、使用すべきオブジェクトが判断され、そのオブジェクトが作成または取得されて、アプリケーションコードに与えられます。これにより、アプリケーションコード内のオブジェクトが、そのオブジェクトを作成するAPIに依存しなくなります。

 IoCと依存性注入という概念の利点は、制御のサブバージョン(subversion of control:SoC)バイジェクション(bijection)の機能を備えたSeamにはっきりと表れています。IoCとSoCの違いは、オブジェクトを注入(inject=インジェクト)できるだけでなく、その逆方向の注入(out-ject=アウトジェクト)もできる点にあります。IoCコンテナは単一のインスタンスを作成または再使用し、そのリソースを必要とするオブジェクトにインジェクトします。アウトジェクトでは、注目すべき状態が含まれているオブジェクトを、後でインジェクトに使用するためにコンテナに入れることができます。IoCではリソースをオブジェクトに注入するだけですが、Seamでは両方向の注入が可能です。

 Seamはインジェクションを遂行するために次のアノテーションを使用します。

  • @Name@Role
  • インジェクトできるクラスを識別します。
  • @In
  • オブジェクトをインジェクトします。
  • @Out
  • インジェクトされたオブジェクトで行われた変更を、それを確認する必要があるかもしれない他のオブジェクトに公開します(注:Springにはこれに相当するアノテーションはありません)。
  • @Scope
  • Seamコンポーネント(@Nameで識別)を宣言的状態管理フレームワークに入れます。
  • @Scope(CONVERSATION)
  • Seamコンポーネントを状態管理フレームワークの対話レベルに入れます。

 Seamでは、EJB 3.0のアノテーションとSeam独自のアノテーションを広範囲にわたって使用します。データベースマッピングとその他のAPIコンフィグレーション情報をアノテーションで指定することにより、サードパーティのフレームワークを使用する際の煩雑さが軽減されます。また、コードが別のXMLファイルに分離されないので、コードが理解しやすくなります。

 SoCと宣言的状態モデルの組み合わせは非常に強力です。これにより、対話的なやり方でコーディングを行うことができます。複雑なページフローも簡単に作成することができ、特に、ネイティブな統合を実現しているJBossのjPDL製品を使用すればさらに効率が上がります(図2を参照)。古いプログラミングモデルではBPMタイプのツールをうまく活用できませんでしたが、JBossのjBPMはこのプログラミングモデルにたやすく適合します。

図2 ワークフローの管理:SeamではjPDLを通じてグラフィカルにページフローを管理できる。jPDLはjBPMでのワークフロー管理にも使われる
図2 ワークフローの管理:SeamではjPDLを通じてグラフィカルにページフローを管理できる。jPDLはjBPMでのワークフロー管理にも使われる

Seamによるページフローの管理

 Seamでページフローを管理する方法は2通りあります。1つはごく初歩的な方法で、ボタンを押すと、文字列が渡され、その文字列が次のページにマップされるという仕組みです。この文字列を返すルーチンに、複雑なページフローロジックが記述されています。次に示す例では、基本的なXMLファイルを使用しています。これは「ステートレスナビゲーションモデル」とも呼ばれます。

<navigation-rule>
    <navigation-case>
        <from-outcome>editCustomer</from-outcome>
        <to-view-id>/editCustomer.jsp</to-view-id>
    </navigation-case>
    <navigation-case>
        <from-outcome>selectCustomer</from-outcome>
        <to-view-id>/findCustomer.jsp</to-view-id>
    </navigation-case>
    <navigation-case>
        <from-outcome>findCustomer</from-outcome>
        <to-view-id>/findCustomer.jsp</to-view-id>
    </navigation-case>
</navigation-rule>
<navigation-rule>
    <from-view-id>/editCustomer.jsp</from-view-id>
    <navigation-case>
        <from-outcome>find</from-outcome>
        <to-view-id>/findCustomer.jsp</to-view-id>
    </navigation-case>
</navigation-rule>

 Seamのもう1つのページフローモデルは「jPDL」と呼ばれます。jPDLは、プロセス定義言語を定義するXMLファイルです。jPDLには優れたグラフィカルインターフェイスが用意されており、複雑なページフローを扱う際に大いに役立ちます。

実験の開始

 Bruce Tateは『Beyond Java』という著書の中で、あるJ2EEアプリケーションを何回かの週末を費やして完成させた後に、同じアプリケーションをRuby on Railsで構築してみたら1回の週末で済んでしまったというエピソードを紹介しています。私も本稿で同様の試みをしたいと思います。私はこの3年半、COBOLからJ2EEにリファクタリングされた大手レンタカー会社のWebベースのレンタカーアプリケーションに取り組んできたのですが、これをSeamで書くとどうなるかを考えてみます。なお、この挑戦にあたっては、48〜72時間で終わらせるという目標時間を設定しました。

 もちろん、このサンプルアプリケーションは、私のチームが長年取り組んできた実際の商用アプリケーションと完全に同じものではなく、スケールダウンしたバージョンです。とはいえ、使用している概念は、大抵の大規模な階層化J2EEアーキテクチャの基礎になっている一般的なものです。

 このアーキテクチャでは、「5+1層」のパターンを使用しています。各層はそれぞれ一定の役割を担っています。階層化アーキテクチャでの依存関係の管理の仕方は、それぞれの層が直下の層にしか依存しないようにすることです。上方向の依存関係は有効ではありません。これは依存関係を管理し、大きな開発チームでうまく責任分担し、チーム全体で共通のやり方によって問題群を分析するための効果的な方法です。

 各層はデータを転送する必要があり、そのデータはほとんど同じ形式になっています。そのため、私は「+1層」を作成し、すべての層のすべてのコンポーネントがそれに対して依存関係を持てるようにしました。この層には、実際のロジックが入っていないデータファイルが含まれています(図3を参照)。

図3 階層化アーキテクチャ:5+1階層アーキテクチャの各層はこのようになっている。コンポーネントはそれぞれの層に置かれ、依存関係はコンポーネント間で識別される
図3 階層化アーキテクチャ:5+1階層アーキテクチャの各層はこのようになっている。コンポーネントはそれぞれの層に置かれ、依存関係はコンポーネント間で識別される

 図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の特定のインスタンスを選択したりするためのページです。
図4 コードの生成:基本データベーステーブルであるLocationテーブルに対してコードジェネレータを実行した様子。2つのJSFページと、それらのJSFページをサポートするためのSeamコードが生成される。「editLocation.jsp」はLocationテーブルの行を作成または修正するためのWebページを提供する。「findLocation.jsp」はLocationテーブルの行を検索したり、それらの行をページングしたり、それらの行の1つを選択するためのWebページを提供する
図4 コードの生成:基本データベーステーブルであるLocationテーブルに対してコードジェネレータを実行した様子。2つのJSFページと、それらのJSFページをサポートするためのSeamコードが生成される。「editLocation.jsp」はLocationテーブルの行を作成または修正するためのWebページを提供する。「findLocation.jsp」はLocationテーブルの行を検索したり、それらの行をページングしたり、それらの行の1つを選択するためのWebページを提供する

 生成されたコンポーネントをコンパイルしてJBossにデプロイすれば、Location、CarClass、Customer、Reservationの各テーブルの検索、ページング、追加、削除、更新が可能になります。ここまでの作業には1時間もかかりませんでした。

 しかし、私は何もかも気に入りませんでした。例えばReservationテーブルでは、ピックアップとドロップオフの場所の主キー、カークラス、顧客を予約に関連付けるためには、これらの情報を入力しなければなりませんでした。また、Reservationオブジェクトでは、これらのクラスを参照するときにオブジェクトではなく整数を使用していました。私は、この関係をもっとうまく管理するコードを生成したいと考えました。このようなコードが実際に生成されている例を見たことがあったので、それが可能なことは分かっていました。そこで、データベースで外部キーを使うことにしました。

 CarClassへの外部キーを作成し、コードを生成してみたところ、その結果は満足のいくものでした。この段階で、予約作成テーブルにアタッチされたボタンをクリックすると、「findCarClass.jsp」ページが呼び出されるようになりました。このページからCarClassオブジェクトを検索し、Reservationオブジェクトに関連付けたいCarClassを選択することができます(図5を参照)。ここまでは1時間足らずで完了しました。

図5 車の選択:このスクリーンショットは、ユーザーが予約作成ページからカークラスを選択するところを示している。予約作成ページから「findCarClass.jsp」ページを呼び出し、[Find]ボタンをクリックして検索を行い、目的のカークラスの横の[Select]ボタンをクリックする
図5 車の選択:このスクリーンショットは、ユーザーが予約作成ページからカークラスを選択するところを示している。予約作成ページから「findCarClass.jsp」ページを呼び出し、[Find]ボタンをクリックして検索を行い、目的のカークラスの横の[Select]ボタンをクリックする

 この時点では、外部キーをあと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時間かかっており、しかも機能は劣っています。

2つの実装アプローチの比較

 Seamの実験で節約されたコーディング量を把握するためには、アプリケーションの各部分を見比べていくのが良いでしょう。

例1

 元のアプリケーションではHttpSessionの状態の保守に多大な労力が投入されていました。HttpSessionのラッパーがあって、状態の追加とイベントとの関連付けを行っていました。イベントが発生すると、このラッパーがHttpSessionをクリーンアップして、そのレベルと下位レベルのイベントをすべて除去していました。Seamでは、こうしたものを書いたり保守したりする必要はまったくありません。

 具体的なコードを見てみましょう。元のアプリケーションでは、SessionManager(HttpSessionラッパー)を使ってhttpSessionからXDelegateを取得します。XDelegateがない場合、または正しい型でない場合は、新しいインスタンスを作成してhttpSessionに入れます。EventLevelはScreenなので、別の画面に移動すると、これはhttpセッションから自動的に除去されます。

XDelegate delegate = getDelegate(request);

…
protected XDelegate getDelegate(HttpServletRequest request)
{
Object delegate = SessionManager.getAttribute(request, DELEGATE_KEY);
    if ((delegate == null) || (!(delegate instanceof XDelegate)))
    {
        delegate = new XDelegate();

SessionManager.setAttribute(
    request, DELEGATE_KEY, delegate, EventLevel.Screen);
    }

    return (XDelegate) delegate;
}

 Seamでは、このコードは次のようになっています。

@In(value="xDelegate", create=true) @Out
XDelegate delegate;

…
@Name("xDelegate")
@Scope(PAGE)
public class XDelegate implements AbstractDelegate{
…
}

 @InはXDelegateをオブジェクトにインジェクトします。create=trueとあるので、XDelegateが存在しない場合は自動的に作成されます。さらに、@Scopeによってこのインスタンスがページレベルで関連付けられます。そのため、新しいページに移ると古いXDelegateがクリーンアップされ、必要なときに新しいものが生成されます。値の受け取りを確認したり、値が正しいインスタンスかチェックするために、何度もコードを書く必要はありません。セッションマネージャに対するインターフェイスを管理するためにコードを書いたり、それをデバッグしたり、オブジェクトが確実にクリーンアップされるように処置したりする必要はありません。Seamの例では、管理およびインジェクションの対象にするオブジェクトと、インジェクションまたはアウトジェクションを行いたいインスタンスにアノテーションを付けるだけ、適切な機能が実現されます。

例2

 元のアプリケーションは、60人以上の開発者から成るチームで取り組んでいましたが、統一的な体系がないためによく問題が発生しました。このアプリケーションでは、動作結果を1つの場所に記録するためにReturnResultsオブジェクトが頻繁に使われていました。データに対して行われた動作の結果を収集して返すために、いたるところでReturnResultsオブジェクトが使われていました。Map、SessionKey、securityの各オブジェクトも同じように厄介な問題を引き起こしました。この3つのオブジェクトは、層と層の間を結ぶすべてのメソッドで何度も受け渡され、その間に膨大な数のメソッドを通過します。メソッド呼び出しチェーンの中の一部のメソッドはこれらのオブジェクトを必要としますが、大部分のメソッドは必要としません。それでも次の層で必要になるかもしれないので、やはりこれらのオブジェクトを渡してやる必要があります。その結果、メソッドシグネチャにはそのメソッドに直接関係のないオブジェクトがあれこれ追加されることになります。しかし、Seamを使用すれば、これらのオブジェクトを必要なところだけにインジェクトできます。オブジェクトを宣言的管理状態の一部にするには、そのオブジェクトを@Nameで指定します。新しいオブジェクトを作成したり破棄したりするタイミングは@Scopeで指定します。ReturnResultsオブジェクトにもこの処理を適用できます。つまり、必要なところでReturnResultオブジェクトをインジェクトし、変更をアウトジェクトすることができます。

対話状態の管理

 対話を開始するには@Begin、対話を終了するには@Endというアノテーションを使用します。@Beginにより、対話パラダイムを用いたコーディングが可能になります。表2に、今回のサンプルアプリケーションの要件を示します。

表2 サンプルアプリケーションのビジネスロジックを設計するために使われるシステム要件
ユーザーシステム
1. ユーザーが予約の作成を示す。2. システムが予約作成インターフェイスで応答する。
3. ユーザーが顧客を検索する。4. システムが顧客の一致条件をリストする。
5. ユーザーが顧客を識別する。6. システムが顧客情報を新しい予約に関連付ける。
7. ユーザーがピックアップの場所を検索する。8. システムが場所の一致条件をリストする。
9. ユーザーがピックアップの場所と時刻を識別する。10. システムがピックアップの場所と時刻を予約に関連付ける。
11. ユーザーがドロップオフの場所を検索する。12. システムが場所の一致条件をリストする。
13. ユーザーがドロップオフの場所と時刻を識別する。14. システムがドロップオフの場所と時刻を予約に関連付ける。
15. ユーザーがカークラスを検索する。16. システムがカークラスの一致条件をリストする。
17. ユーザーがカークラスを識別する。18. システムがカークラスを予約に関連付け、見積もり料金を計算する。

 これはシステムの出資者にとってビジネス価値のあるアイテムを作成するための一連のやりとりです。しかし、これをどうやってコードにマップするのでしょうか。元のアプリケーションでは、ステートフルセッションBeanのインターフェイスに相当な量のロジックがマップされていました。

public CustomerListTR retrieveCustomers()
public ReservationTR addCustomer(ICustomerVO cust)
public AllLocationsTR retrieveAllLocations()
public ReservationTR assignPickUp(ILocationVO loc, Date dat)
public ReservationTR assignDropOff(ILocationVO loc, Date dat)

 これらのメソッドの背後には、コンポーネントを取得したり、何かをするように要求したり、状態を管理したりするコードが大量に存在します。元のアプリケーションでは、外部システムとのやりとりの大部分で非同期メソッド呼び出しを使用していました。パフォーマンス上の理由から、ユーザー側が開始する何回かのユーザー/システム間のやりとりにわたって、非同期呼び出しの応答を保持する必要があったのです。私はその状態をhttpセッション、ステートフルセッションBean、およびエンティティBeanに格納することを試みましたが、どのソリューションも対話向きではありません。これらの方法では、オブジェクトを状態管理システムに入れ、不要になったらそこから取り除くためのコードを書く必要があります。そして、明示的に処理されない方法でユーザーが対話を終了した場合には、厄介なバグが発生する可能性があります。

 Seamアプリケーションでは、オブジェクトが必要になったらインジェクトし、必要に応じて対話を開始したり終了したりします。そのため、対話状態に対応していない状態管理モデルで対話状態を管理するという余計な仕事に煩わされず、本来のビジネスロジックに専念できます。Seamにより、表2の要件によく似た形のコードを書くことができるのです。

@In(value="reservationFinder", required = false)
private transient ReservationFinder reservationFinder;

@Begin(join = true)
@IfInvalid(outcome = Outcome.REDISPLAY)
public String create() {

@IfInvalid(outcome = Outcome.REDISPLAY)
public String update() {

@End(ifOutcome = "find")
public String delete() {

@End(ifOutcome = "find")
public String done() {

@In(create = true)
private transient CustomerEditor customerEditor;

public String customer() {

@Begin(join = true)
public String selectCustomer() {

@In(create = true)
private transient CarclassEditor carclassEditor;

public String carclass() {

@Begin(join = true)
public String selectCarclass() {

@In(value="pickUpLocationEditor",create = true)
private transient LocationEditor pickUpLocationEditor;

public String pickUpLocation() {

@Begin(join = true)
public String selectPickUpLocation() {

@In(value="dropOffLocationEditor",create = true)
private transient LocationEditor dropOffLocationEditor;
public String dropOffLocation() {

@Begin(join = true)
public String selectDropOffLocation() {

フロントエンド

 最後に、このアプリケーションのフロントエンドに注目して、Seamがどのような働きをしているかを確認しましょう。元のアプリケーションのAppCoord層(図3を参照)は、Struts、Swing、SOAP、JMSなど、何種類かのクライアントと通信します。この負担を軽減するために、現在の要求に関係するデータは値オブジェクト(VO)から転送オブジェクト(TO)に移されます。元のアプリケーションでは、VOへの修正を隠すためにTOを設け、VOをAppCoord層のクライアントから切り離すために抽象層を設けています。これにより、現在の操作を実行するために必要なデータだけを交換することになります。

 TOは、メッセージ、問題、デコレーションインジケータを各フィールドに関連付けられるようにするために、StringとDateをはじめ、すべての基本型をラップしています。これらのラッパーにより、この情報をクラスレベルで関連付けることができます。この情報をReturnResultとVOからTOとラッパーに移すためのコードを作成するにはかなりの手間がかかります。

 例えばStrutsなら、Formオブジェクト、Actionオブジェクト、およびJSPそのものを作成しなければなりません。Strutsアクションに対してデータを表示できるようにするには、一群のデータを1つ以上のVOからTOに移します。また、エラーメッセージとフィールドデコレータインジケータをTOとラッパーに移します。TOをStrutsアクションに渡すと、Strutsアクションはそのすべての情報をStrutsベースのFormオブジェクトに移します。このようなデータ移動をすべてコーディングし、デバッグと保守を行う必要があります。

 Seamでは、こうしたデータ移動のためのコードがそっくり不要になります(表3を参照)。SeamはJSFへの拡張機能を提供するので、Strutsアクションの必要性もなくなります。Seamフレームワークの利点は、もし必要ならTO概念を追加できるところにあります。JMSベースやSOAPベースのメッセージ内のデータを共有するつもりなら、私はおそらくVOを使用しないでしょうが、SeamコンポーネントとJSFの統合の威力には注目すべきものがあります。

表3 StrutsとSeamとでの行数の比較
Strutsのクラス(顧客をリストする) 行数
customerList.jsp41
CustomersAction.java65
CustomerForm.java181
CustomersForm.java61
CustomerListTR.java49
合計397
Seamのクラス(顧客を取得し、リストし、選択する)行数
findCustomer.jsp193
合計193

Seamを縫い合わせる

 Seamの好きなところの1つは、Java EE 5で開いたままになっている穴を埋めてくれるという点です。通常は開発者がコードを書くことでAPIとフレームワークを管理していますが、Seamではこの管理が自動的に行われるので、開発者はもっと大局的な問題に時間を使うことができます。しかも、特定のアーキテクチャを強いられることはありません。

 このAPI管理だけでも大きなメリットですが、Seamのコンテキストによる状態管理は、要件リストによく似た形のコーディングを可能にします。これにより、アプリケーションサーバーでJava仕様がどのように実装されているかということに気を取られず、実際の機能に専念できるようになります。また、EJBの複雑さに頭を悩ませなくて済むという利点もあります。

 とはいえ、今回作成したSeamアプリケーションにも不満な点がいくつかあります。生成されるコードで分離の問題をもっとうまく処理できる可能性があります。例えばEditorとFinderは、それぞれの担当処理をこなしているのはもちろんですが、これらのコンポーネントのメソッドの多くは、ページフローモデルで次に表示すべきページを決定するのに使われる文字列を返します。つまり、編集と検索だけでなく、ページフローとページングも行っているのです。

 Selectorはリストからオブジェクトを選択しやすくするだけでなく、ボタンやページタイトルのラベルの決定も行います。私が最も気に入らないのは、LocationSelectorがReservationEditorにコールバックすることです。これは正しいオブジェクト関係についてのオブジェクト指向の基本原則に違反しているのではないでしょうか。個人的には、検索を行ってファインダに一覧表示するSQLの作成はバックエンドのコードで取り扱うべきであり、次に表示するページを決定するコードからは分離させた方がいいと考えています。今回のアプリケーションのSelectorオブジェクトは3種類のことを行っています。このようなコードは異なる3つのオブジェクトに分けるべきです。

 生成されたコードはdivタグを使ってレイアウトを行うので、IEでの見た目はよくなかったものの、Mozilla Firefoxでの見た目は良好でした。なお、現在のSeamはベータ版なので注意してください(※2006年4月、執筆当時)。

 こうした点を別にすれば、Seamは統合やビジネスロジックやフロントエンドを実現するために必要なコードを減らすことができます。今回取り上げたアプリケーションのCustomerオブジェクトでは、実に800行の差が出ました。何百ものキードメインオブジェクトと何百万行ものビジネスロジックがある大規模なシステムでは、コードの行数を40〜60%削減できると思います。コードの行数が増えるほどアプリケーションの保守コストが大きくなることを考えれば、Seamへの投資はすぐに回収できるはずです。

著者紹介

Mark Smith(Mark Smith)
Valtech Technologies, Inc.(www.valtech.com)取締役。軍需産業に13年間従事したのち、コンサルティング業界に転身。現在はValtechの取締役として、100人以上の開発者と数百万行のコードを抱えるJ2EEの全プロジェクトを技術面で監督。
Markと彼のValtechチームはこれまで80人以上のCOBOL/ウォーターフォール型プログラマにJava/J2EEとAgileの方法論を指導して実績を上げている。また、Markは上級スタッフメンバらと共に、400人の開発者を擁するValtech社の戦略的指針を検討する立場にある。
プライバシー ジャパン・インターネットコム版
【プライバシー ジャパン・インターネットコム版】
認証がオンラインビジネスの鍵である理由(11月4日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/