SpringFrameworkを使ったLegacyCode(過去の遺物)との付き合い方
ちょっと見るだけで途方にくれるようなLegacyCodeをどうしよう…というのが昨年末の課題だったのですが、いろいろな方にヒントをいただいたおかげでだいぶ道筋が見えてきました。
(以下、JDK1.3という縛りがあるので今回はSpringFrameworkを使っていますがS2でも同じようなことができると思います。)
基本はEclipseのリファクタリングメニュー駆使
めちゃめちゃ長いビジネスメソッドは、何はともあれ、まずリファクタリング。
LegacyCodeはテスタビリティなんて配慮されていないので一部をモックで置き換えてテストを書くことが非常に難しいです。
なので、実際にDBまでアクセスして機能を動かすテストを数パターン書いたら、あとはツールを使ってリファクタリングします。
その際の作業は基本的に以下の繰り返しです。
- 元のクラスの長いStatelessなメソッドの一部を選択し、「リファクタリング-メソッドの抽出」(これで同一クラス内のメソッドとして処理の一部が切り出されます。同一クラス内に重複コードがあれば、一緒に切り出してくれます)
- 新しいクラスを作って、抽出したメソッドの内容を移します。javadocコメントもちゃんと書いておきます。
- 新しいクラスに作ったメソッドについて「リファクタリング-インタフェースの抽出」(クラスを先に作っているのは、Eclipseでは「リファクタリング-インタフェースの抽出」の方が「インタフェースを実装したクラスの新規作成」よりちゃんとjavadocコメントを引き継いでくれるためです)
- 元のクラスに、作ったインタフェースを保持するフィールドを作成します
- 抽出したメソッドの内容をフィールドで保持したインタフェースの呼び出しに置き換えます
- 「ソース-getter/setterの生成」で、フィールドに対するsetter/getterを生成します(DIコンテナのSetterInjectionで使います。また、DIコンテナを使わない単体テストでも任意の実装クラスをsetできます)
これで、元の機能を維持しつつ、テストがしやすく見やすいコードになります。
見やすいかどうかは意見が分かれるところだと思いますが、テストがしやすいことは間違いありません。簡単にモックをセットしてテストができるのが嬉しいポイントです。
インタフェースもクラスも増えるので、クラスの責務を簡潔に一言で言い表せる範囲に限定して的確な名前を付けることと、細かくパッケージを分けてパッケージあたりのクラス数を抑えることが重要になります。(パッケージを分ける際にはEclipseの「リファクタリング-移動」が便利)
機能を変更する場合、こうやって作ったインタフェースに対して(リファクタリングで切り出したのとは)別の実装クラスを作ります。こうするとsetする実装クラスを切り替えることでいつでも新旧それぞれの機能を使えるようになります。
メソッドを分割するときは、DBなどの外部システムを操作する部分とそれ以外の部分とをきっちり分割します。また、処理フローの制御(分岐や繰り返し)・プロシージャ(処理)・ファンクション(なにかの計算をして値を返すもの)を意識して分割します。
(もちろん、常に1メソッドを1クラスにするわけではなく、同じような責務のメソッドがあればいくつかまとめて1クラスにします。)
without EJB化
StatelessSessionBeanはEJBでなくなって困ることはほとんどないので、上記のとおりインタフェース+POJOに内容を移してからリファクタリングします。EJB内ではPOJOの呼び出しのみを行います。
(StatelessSessionBeanの中身がスレッドセーフでない場合は多少注意が必要ですが、それほど問題になることはないでしょう)
今回はやりませんが、CMP EntityBeanはHibernateで置き換えればwithout EJB化もそれほど難しくなさそうです。
StatefulSessionBeanをなんとかする
テストが難しいStatefulSessionBeanは、HttpSessionで置き換えてしまいます。
- StatefulSessionBean本体を継承してRemoteインタフェースを実装するクラス(偽StatefulSessionBean)を新しく作ります。(といってもEclipseに任せればコードを書くところはほとんどありません)
- StatefulSessionBean本体のフィールドに対応するsetter/getterがなければ、偽StatefulSessionBean上に作成します。
- StatefulSessionBeanをhandleから取得/handleを格納/create/removeしているコードを一つのユーティリティクラスに集めます。
- 例によって、ユーティリティクラスからインタフェースを抽出し、インタフェースと元のStatefulSessionBean版実装クラスとを分離します。
- 抽出したインタフェースについて、新しいHttpSession版の実装を作成します。
- create時には偽StatefulSessionBeanをnew()してejbCreate()を呼び出します
- HttpSessionにhandleの代わりに偽StatefulSessionBeanを格納/取得します
- remove時にはejbRemove()を呼び出し、HttpSessionからremoveAttributeします
これで、少なくともテスト時にはHttpSessionのMockと新しいHttpSession版の実装を使うことができます(テスト時には偽StatefulSessionBeanをnew()してフィールドの内容をsetしてからHttpSessionに格納します)。
webコンテナ上で動かすときもHttpSession版の実装を使うことができます。(リリース時もHttpSession版の実装を使うには更に検証が必要ですが。)
DBアクセスをなんとかする
JDBCの処理はSpringのJDBCテンプレートを利用するHelperクラス群を作ってその中で行います。(Springに依存する部分はなるべく限定したいので、このJDBCヘルパーとシステムの中核にあたるFactoryクラスとHibernateSupportの子クラスのみ、としています。Springをメンバー全員にちゃんと教えるのが大変なためでもありますが。)
RowMapperは共通の実装(ResultSetのメタデータを元にReflectionで処理)としておき、呼び出し側はPOJO(ValueObject)かMapで受け取るものとしています。
機能変更部分や機能が変わらずリファクタリングしただけの部分は、Helperクラスを使うことで元のSQLをそのまま実行でき、しかも以前より簡単なコードでパラメータをセットしたり結果を取得したりできるようになりました。
トランザクション管理をなんとかする
JDBCベースのアプリケーションではコネクションの切れ目がトランザクションの切れ目なので、LegacyCodeでは同一トランザクションの処理は1メソッド内に全部書かれていたりします(泣)。
まずはトランザクション管理を気にせずサクサクと(上に書いた手順で)メソッド抽出・クラス分割・インタフェース抽出をしていきます。DBアクセス部分はDAO実装クラス内のフィールドとしてJDBCヘルパーのインタフェースを持つようにし、それを使うように書き換えます。
それから各クラスのjavadocコメント内にSpringのbean定義用のxDocletタグを、各メソッドのjavadocコメントにトランザクション制御用のメタデータを書いていきます。
自動生成するSpringのbean定義ファイル(applicationContext.xml)とは別のbean定義ファイルを作り、そこでDefaultAdvisorAutoProxyCreator・TransactionAttributeSourceAdvisor・TransactionInterceptor・AttributesTransactionAttributeSourceを定義します。
この「自動proxy&メタデータ」というのがSpringFrameworkでトランザクション制御をする方法では一番良いように思います。XMLのメンテナンスに悩むことなく細かな制御ができるので。
Springのbean定義はxDocletで自動生成した方が楽です。Springでは定義ファイルの分割が簡単なので、自動生成するもの、自動生成しないけれど動作環境によって変えるもの(DataSourceの定義など)、自動生成せず動作環境で変えることもないもの(トランザクション管理の定義など)と分けて、さらにテスト用は別途作成しています。
HibernateとJDBCを同一トランザクション中で混在させたい場合は、トランザクション管理はHibernate用のクラスで行います。(積極的に混在させたいとは思わないけれど)