SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

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

Seamによるステートフルなオブジェクトの注入

  • X ポスト
  • このエントリーをはてなブックマークに追加

実験の開始

 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つの実装アプローチの比較

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Mark Smith(Mark Smith)

Valtech Technologies, Inc.(www.valtech.com)取締役。軍需産業に13年間従事したのち、コンサルティング業界に転身。現在はValtechの取締役として、100人以上の開発者と数百万行のコードを抱えるJ2EEの全プロジェクトを技術面で監督。Markと彼のValte...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/484 2006/08/22 15:52

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング