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に、今回のサンプルアプリケーションの要件を示します。
ユーザー | システム |
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の統合の威力には注目すべきものがあります。
Strutsのクラス(顧客をリストする) | 行数 |
customerList.jsp | 41 |
CustomersAction.java | 65 |
CustomerForm.java | 181 |
CustomersForm.java | 61 |
CustomerListTR.java | 49 |
合計 | 397 |
Seamのクラス(顧客を取得し、リストし、選択する) | 行数 |
findCustomer.jsp | 193 |
合計 | 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への投資はすぐに回収できるはずです。