古くなったドキュメント
ときには開発者がクラスからコードを削除した結果、あるパッケージが設計のルールに反して別のパッケージに依存しなくなることがあります。先ほどとは逆のことをやってみましょう。「biz.xsoftware.client2.Client.java」ファイルを削除し、ビルドをもう一度実行するのです。次のエラーが表示されます。
Package name=client2 has a dependency declared that is not true anymore. Please erase the dependency <depends>phoneApi</depends> from package=client2
メッセージの指示に従ってdependsノードを削除し、ビルドを再実行すると、コードは設計を満たすようになるのでビルドは正常に終了します。最後に「Client2.java」ファイルを削除し、ビルドをもう一度実行します。
Package name=client2 is unused. Full package=biz.xsoftware.client2
以上で、Verifydesignが依存関係の制約を強制して、設計に常に最新の状態を反映する仕組みについての説明は終わりです。この機能は最近になってVerifydesignに追加されたものであり、最初に私たちがプロジェクトにVerifydesignを実行したときは、「design.xml」にあった約50行のうち17行を削除しなければなりませんでした。その結果、「パッケージ設計」の読みやすさが大幅に改善されました。早い話が、設計のドキュメントが常に最新の状態を反映していることが保証されると思ってかまいません。
Ant-contribをビルドに結合する
次に、Verifydesignによる依存関係の強制が確実に適用されるようにするため、Verifydesignをビルドに結合する必要があります。これを行うのは、「verifydesign/package/bldfiles/build.xml」ファイルの以下の行です。
<property name="antcontrib.location" location="${tool.dir}/ant-contrib" /> <path id="antcontrib.lib"> <fileset dir="${antcontrib.location}"> <include name="**/*.jar" /> </fileset> </path> <taskdef resource="net/sf/antcontrib/antlib.xml" classpathref="antcontrib.lib" />
この部分で、すべてのant-contribタスクが読み込まれます。続いて、Verifydesignを単独のjarに使うため、以下の行を追加します。
<verifydesign jar="${jardist}/${jar.name}" design="bldfiles/design.xml" />
詳細については、ant-contribのホームページに用意されたドキュメントを参照してください。
Verifydesignをレガシーシステムで使う
他人が作成したソフトウェアを引き継いだり、既存の製品に携わるチームに後から加わったりするのはよくあることです。大抵の場合、引き継ぐのは図1のようなシステムです。依存関係を整理したくならないでしょうか。この目的にもVerifydesignが役立ちます。
最初のステップとして、現在の実装のままでビルドを正常に実行できる設計ファイルを定義します。この時点ではAPIを気にする必要はありません。最初に作成する設計ファイルは次のようになります(ダウンロードサンプルの「verifydesign/legacy/bldfiles/design.xml」ファイルを参照)。
<design> <package name="componentA" package="biz.xsoftware.componentA" needdeclarations="false" needdepends="false"/> <package name="componentB" package="biz.xsoftware.componentB" needdeclarations="false" needdepends="false"/> <package name="componentC" package="biz.xsoftware.componentC" needdeclarations="false" needdepends="false"/> <package name="componentD" package="biz.xsoftware.componentD" needdeclarations="false" needdepends="false"/> <package name="componentE" package="biz.xsoftware.componentE" needdeclarations="false" needdepends="false"/> </design>
このコードのneeddepends="false"属性は、そのパッケージが依存関係を宣言しなくてもよいことを指定する手段です。同様に、needdeclarations="false"属性は、依存する側のパッケージが依存関係を宣言する必要がないことを指定します。これらの属性はjavax.swingの使用時に特に便利です。needdeclarations="false"を指定するだけで、swingに依存するコンポーネントの1つ1つにswingを書く手間が省けるからです。さて、今度はverifydesign/legacyディレクトリに移動して、以下のコマンドを実行してビルドを行います。
ant --f bldfiles/build.xml
次のステップに進みましょう。レガシーシステムを改良する際のビジネス目標は、ビジネスの価値を保ちながら設計を「段階的に」整理することです。例えば、リリース1の実装に含まれている、1つの望ましくない依存関係を標的にして整理していきます。図7の赤い矢印は、最初に削除しようと考えている依存関係を示します。

この依存関係を削除するには、コンポーネントEがコンポーネントCに依存する原因となっているすべてのクラスを洗い出す必要があります。まず、前掲のコードのcomponentEからneeddepends属性を削除します。
<design> <package name="componentA" package="biz.xsoftware.componentA" needdeclarations="false" needdepends="false"/> <package name="componentB" package="biz.xsoftware.componentB" needdeclarations="false" needdepends="false"/> <package name="componentC" package="biz.xsoftware.componentC" needdeclarations="false" needdepends="false"/> <package name="componentD" package="biz.xsoftware.componentD" needdeclarations="false" needdepends="false"/> <package name="componentE" package="biz.xsoftware.componentE" needdeclarations="false"/> </design>
次に、componentEパッケージが依存しているすべてのものを宣言する必要があります(そうしないと、ビルドがエラーになります)。この例では、他のすべてのパッケージでneeddeclarations属性がfalseに設定されているので、ビルドがエラーになるのはcomponentEが外部パッケージに依存する場合だけです。ここでビルドコマンド「ant?f bldfiles/build.xml」をレガシーシステムで実行し、どのような結果になるか試してください。
componentEがSwingの何かに依存していることが分かるはずです。この依存関係を表現するために、Swingのパッケージを新たに追加し、このパッケージへの依存をすべてのパッケージに許可します(このような設定が望ましくないシステムもあります)。次の行を「design.xml」ファイルの先頭に追加してください。
<package name="swing" package="javax.swing"
subpackages="include" needdeclarations="false"/>
この1行を加えることで、再びビルドが正常に実行されるようになります。ビルドを実行し、設計に違反していないことが確認されました。これでcomponentEパッケージの外部への依存関係が解消されたので、次のパッケージに移ります。同じ方法でcomponentCパッケージの依存関係を検出します。componentCからneeddeclarations属性を削除することで、componentCに依存するコンポーネント(ただしneeddepends属性を持たないもの)に対してその依存関係の宣言を強制します。変更後の設計ファイルを以下に示します。
<design> <package name="swing" package="javax.swing" subpackages="include" needdeclarations="false"/> <package name="componentA" package="biz.xsoftware.componentA" needdeclarations="false" needdepends="false"/> <package name="componentB" package="biz.xsoftware.componentB" needdeclarations="false" needdepends="false"/> <package name="componentC" package="biz.xsoftware.componentC" needdepends="false"/> <package name="componentD" package="biz.xsoftware.componentD" needdeclarations="false" needdepends="false"/> <package name="componentE" package="biz.xsoftware.componentE" needdeclarations="false"/> </design>
これで、componentCに依存するものは、依存関係を宣言しなければならなくなりました。ただし、needdepends="false"属性が設定されているコンポーネント(つまり、コンポーネントE以外のすべてのコンポーネント)はこの限りではありません。ビルドを再実行すると、次のエラーが表示されます。
You are violating your own design.... Class = biz.xsoftware.componentE.SubComponentE depends on Class = biz.xsoftware.componentC.ComponentC The dependency to allow this is not defined in your design Package=biz.xsoftware.componentE is not defined to depend on Package=biz.xsoftware.componentC Change the code or the design
エラーメッセージの指示に基づいて、ComponentCクラスに依存するSubComponentEクラスを編集し、依存関係を修正する必要があります。この時点で、コードは新しい設計に準拠した状態になります。ここで使ったサンプルのレガシープロジェクトにはComponentEとComponentCの依存関係しかありませんが、現実のシステムの場合、おそらく多くの依存関係を整理する必要があるでしょう。Verifydesignは、依存関係を1つ残らず指摘します。
componentEがcomponentCに依存している問題の解決方法は、自分で考えてみてください。ヒントだけ差し上げましょう。componentE内でリスナを使用し、そのリスナをcomponentCのクラス内で実装するのです。デザインパターンを使って依存関係を反転させる方法は、もう1本別の記事が書けるぐらい複雑なテーマなので、ここでは深入りしません。
特記事項
Verifydesignには、まだ触れていない小さな特記事項が1つあります。それは、定数の依存関係をチェックできないことです。例えば、次のような2つのクラスがあるとします。
biz.xsoftware.impl.client.Client biz.xsoftware.impl.phone.PhoneImpl
さらに、「Client.java」は「PhoneImpl.java」内に定義された次のフィールドに依存すると仮定します。
public static final String someConstant = "xyz";
「Client.java」をコンパイルするとバイトコードに"xyz"が組み込まれるため、ClientがphoneImplに依存することを示す情報はどこにもなくなります。Verifydesignはバイトコードを分析して依存関係をチェックするので、このミスを検出できないのです。バイトコード内のClient.classからphoneImplを参照する他のすべての依存関係はVerifydesignによって設計違反として検出されるのですが…。残念ながら、この問題を修正するにはJava言語仕様への追加が必要であり、これはSunにしか実行できません。この問題は時々起こりますが、たとえVerifydesignで検出できなくても、修正そのものは他の依存関係と比べて非常に簡単です。全般的には、VerifyDesignはコードが望ましい「パッケージ設計」に従うことを保証するうえで絶大な威力を発揮します。
この記事では、Verifydesignツールの助けを得て機能するEnterprise Architecture Preservation(EAP)の概要を見てきました。この機能をチームで利用する方法と、レガシーシステムに配備する方法について説明しました。このツールをレガシーシステムに配備すると、それまで見逃されてきた依存関係の多さに大抵驚くことになります。Verifydesignの使用を最初は煩わしく感じるのが普通ですが、数週間もするとこのツールの真価を実感できます。Verifydesignは望ましくない依存関係をすべてのステップで検出してくれます。このような望ましくないパッケージの依存関係はパッケージ設計そのものなので、Verifydesignを使うことで、コードの変更が設計の変更につながる可能性があることにいやでも目を向けさせられます。そのうちに、いつか誰かが、この設計ファイルから見栄えの良い設計図を生成するAntタスクを作成したり、主要なIDEにビジュアル設計機能を統合したりしてくれるかもしれません。
Verifydesignによってセットアップされるアーキテクチャでは、コンポーネント自体の単体テストも非常に容易です。既に、Verifydesignと互換性のあるmocklibというライブラリが用意されています。mocklibにはAPI全体をシミュレートする機能があるため、例えばcomponentEのAPIをシミュレートしてcomponentCをテストするようなことが可能です。これにより、テストスイート内の1つのテストを書き直さなくても、componentCを完全にリファクタリングできます。テストスイートを変更するのは、コンポーネントAPIを変更するときだけです。mocklibについても詳しく調べてみることをお勧めします。
この記事の締めくくりとして、Verifydesignを現在使用しているプロジェクトへのリンクを紹介します。これらのオープンソースプロジェクトをダウンロードし、使われている「パッケージ設計」を参照してみてください。Java Sipプロジェクトには2つのレイヤがありますが、下部レイヤだけを取得すれば、他のコードを手に入れる必要はありません。これは非常によくできています。面白いことに、以下に挙げるプロジェクトはどれもmocklibを利用しています。javasipプロジェクトは、channelmanager API全体(1つのインターフェイスだけでなく)をシミュレートするためにmocklibを使っています。また、Java State MachineプロジェクトとNIO Abstractionプロジェクトも参照する価値があります。
