レベル: 中級 Paul Duvall (paul.duvall@stelligent.com), CTO, Stelligent Incorporated
2007年 7月 10日 皆さんのソフトウェア・アーキテクチャーは思い通りのものになっていますか? ソース・コードに関して言えば、お互いに伝達し合った設計が、必ずしも期待していたとおりに実現されるとは限りません。今回、Paul Duvall が再開する連載「万人のためのオートメーション」では、アーキテクチャー上の逸脱をそれが発生してから見つけるのではなく、JUnit、JDepend、そして Ant を使って作成したテストで事前に見つける方法を紹介します。
私が今まで取り組み、関わってきた数多くのソフトウェア開発プロジェクトでは、ほとんど必ずと言っていいほど 1 つの共通点がありました。それは、考えているアーキテクチャーが実際のアーキテクチャーとは違っているということです。
JDepend (「参考文献」を参照) などのツールで作成したコード・メトリック・レポートを分析すると、コードが確定済みのアーキテクチャーに忠実であるかどうかを効果的に判断することができます。また、コードから UML 図へのリバース・エンジニアリングを行って効果が同じであるかどうかを調べるチームもあれば、IDE を使用してコーディングと同時に同じ成果物を生成する (リアルタイム・リバース・エンジニアリング) チームさえあります。しかし、これらの手法はいずれも事後対策であることには変わらず、レポートやダイアグラムを手作業で検討、分析してアーキテクチャー上の違反を判断する必要があります。そのため、場合によっては後の祭りになりかねません。
例えばリスト 1 に示すように、Ant ビルド・スクリプトの失敗などによって、コードの一部分が意図したアーキテクチャーに違反するたびに通知を受け取ったとしたらどうでしょう。
リスト 1. アーキテクチャー違反によるビルドの失敗
...
BUILD FAILED
...
build.xml:35 Test ArchitecturalRulesTest failed
Total time: 20 seconds
|
 |
この連載について
私たちは開発者として、エンド・ユーザーのプロセスを自動化するために作業しますが、自分自身の開発プロセスを自動化するチャンスは見落としがちです。そこで、連載記事「万人のためのオートメーション」では、実用的なソフトウェア開発プロセスの自動化を検討し、自動化を上手に適用するタイミングと方法を教えます。
|
|
この記事では、ソフトウェアのアーキテクチャーをビルド自動化の利点を生かして事前に分析できるようにする手法を紹介します。さらにサンプルを用いて、JUnit と JDepend の API を併用して定義可能なルールに基づき、ビルド・プロセスを停止する方法も説明します。
もちろん最良の点は、ビルド自動化によって、皆さんと皆さんのチームが確定済みアーキテクチャーに対するソース・コードの違反を開発ライフサイクルの初期段階に発見できることです。それがつまり、アーキテクチャーの管理をするということです。
事が起きてからの対応
図 1 は、Web アプリケーションをビルドする際の一般的なアーキテクチャー・パターンです。このパターンでは、プレゼンテーション (Presentation ) 層 (関連パッケージのグループ) はコントローラー (Controller ) 層に依存し、コントローラー層はドメイン (Domain ) 層とビジネス (Business) 層に依存し、そしてビジネス層はデータ (Data) 層とドメイン層に依存しています。
図 1. 一般的な Web アプリケーションのアーキテクチャー階層
ここまでは何の問題もありませんが、みなさんもきっと経験したことがあるように、このような一般的なベスト・プラクティスの形は日々のソフトウェア開発の混乱に紛れて失われしまうことがあります。実際、それは至って簡単に (しかもあっという間に) 起こりがちです。
図 2 は、上記のサンプル・アーキテクチャーに対する微妙な違反の例で、このアーキテクチャーではデータ層が ビジネス層に少なくとも 1 つの呼び出しを行うようになっています。
図 2. データ層がビジネス層のオブジェクトを呼び出していることによるアーキテクチャー違反
このようなわずかなアーキテクチャーの変更でも、修正を一層困難にする予想外の影響をもたらします。実際、上記の例では、コードの一部を修正するために他の多くの部分も変更しなければならなくなっています。例えば、ビジネス層クラスのいくつかのメソッドを排除または変更する場合、データ層からも参照を削除しなければなりません。違反が生じれば生じるほど、変更に伴う問題は一層悪化していきます。
JDepend または Macker レポート (「参考文献」を参照) を検討するなどの従来の監視手法を使った場合、意図する設計からの逸脱をどれだけ早く発見できると思いますか? 私と同じように、みなさんも実動ソフトウェアを短時間で作成したいのなら、デリバリーの速さに影響する問題をできるだけ早期に見つけられるのにこしたことはないと思いませんか。
JDepend による簡単な魔術
アーキテクチャー違反を簡単に判断できるようにする手軽なツールの 1 つとして挙げられるのが、JDepend です。このオープン・ソースのツールは長年出回っていて、Ant や Maven との相性が良いだけでなく、一層きめの細かい対話動作を目的としてリッチな Java™ API もサポートしています。ただし前にも述べたように JDepend によるレポートはもともと受動的なので、実際にこのレポートを作成 (および表示) する頻度次第では、修正が極めて困難になるまでアーキテクチャー違反が見過ごされてしまう可能性があります。
 |
求心性結合と遠心性結合
JDepend では、求心性結合は分析されたパッケージに対して依存しているパッケージの数として表されます。例えば、ロギング・フレームワーク、あるいは Struts のような Web フレームワークを使用している場合、パッケージは高い求心性結合を持つと予想されます。なぜなら、コード・ベース全体で多数のパッケージがこれらのフレームワークに依存するはずだからです。一方、求心性結合とは逆の遠心性結合は、分析されたパッケージが依存しているパッケージの数として表されます。つまり、そのパッケージが持つ依存パッケージの数です。
|
|
アーキテクチャーのアサーション
アーキテクチャーが不適切に変更されているかどうかを事前に判断するということは、実際には特定パッケージの結合を調べるということです。ソフトウェア・アーキテクチャーに含まれる重要なパッケージの求心性結合と遠心性結合の両方を監視し、期待される値との差がないかどうか注意することで、間違った修正を容易に警告することができます。
図 2 に示した修正を例に挙げると、データ層がビジネス層と直接やり取りするようになっていることから、データ層の新しい遠心性結合は 0 より大きくなります。この結合はもちろん、データ層に常駐するクラス内にある単純な import (そして import の使用) によるものです。幸いなことに、JUnit、JDepend の素晴らしい API、そしてちょっとしたビルドの魔法を使えば、このような問題を簡単に突き止められます。
ビルド (Ant または Maven など) のコンテキスト内では、JDepend の API を利用する JUnit テストを実行して、結合値が変更されることを事前に検出できることがわかっています。しかも、検出した変更が有効でない場合には、ビルドを停止することもできます。申し分のない事前対策だと思いませんか?
JUnit テストを作成して適切に JDepend を構成するための最初のステップは、リスト 2 のとおりです。
リスト 2. JUnit での JDepend の設定
import junit.framework.TestCase;
import jdepend.framework.JavaPackage;
import jdepend.framework.JDepend;
public class ArchitecturalRulesTest extends TestCase {
private static final String DIRECTORY_TO_ANALYZE = "C:/dev/project-sandbox/brewery/classes";
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
private JDepend jdepend;
private String dataLayer = "com.beer.business.data";
private String businessLayer = "com.beer.business.service";
private Collection dataLayerViolations = new ArrayList<String>();
public ArchitecturalRulesTest(String name) {
super(name);
}
protected void setUp() throws IOException {
jdepend = new JDepend();
jdepend.addDirectory(DIRECTORY_TO_ANALYZE);
// Calling the businessLayer from the dataLayer is a violation
dataLayerViolations.add(businessLayer);
}
|
リスト 2 の内容は盛りだくさんなので、重要な点を以下にまとめます。
- 2 つの JDepend クラス、
jdepend.framework.JavaPackage と jdepend.framework.JDepend は必須です。
- 分析するソース・コードの場所は、
DIRECTORY_TO_ANALYZE 定数に定義します。このディレクトリーをスキャンするために JDepend が呼び出すのは Jdepend.addDirectory です。これは (setUp() メソッドに従った) フィクスチャーによって行われます。
- 分析するパッケージは、「層」の
String に定義します。
-
dataLayerViolations Collection は、意図されたアーキテクチャーに対する違反を示すために businessLayer String (パッケージを表す) を追加します。
上記の 4 点を用いて、JDepend を効果的に設定し、特定のコード・ベースにその魔法が利くようにしました。次に必要なのは、結合値の変更を明らかにするための正確なロジックです。
リスト 3 の testDataLayer() テスト・ケースは、このアーキテクチャー・アサーションの魔力の中心部分です。このメソッドが dataLayer に対する違反があるかどうかを判断します。isLayeringValid() メソッド (次のリスト 4 で定義) が false を返した場合はアーキテクチャー違反があるということなので、このテスト・ケースは失敗と見なされます。
リスト 3. JDepend 風のテスト・ケース検証
public void testDataLayer() {
if (!isLayeringValid(dataLayer, dataLayerViolations)) {
fail("Dependency Constraint failed in Data Layer");
}
}
|
リスト 4 は、リスト 3 のテスト・ケースが呼び出すメソッドです。
リスト 4. ループによるパッケージごとの求心性結合の検出
private boolean isLayeringValid(String layer, Collection rules) {
boolean rulesCorrect = true;
Collection packages = jdepend.analyze();
Iterator itor = packages.iterator();
JavaPackage jPackage = null;
String analyzedPackageName = null;
while (itor.hasNext()) {
jPackage = (JavaPackage) itor.next();
analyzedPackageName = jPackage.getName();
Iterator afferentItor = jPackage.getAfferents().iterator();
String afferentPackageName = null;
while (afferentItor.hasNext()) {
JavaPackage afferentPackage = (JavaPackage) afferentItor.next();
afferentPackageName = afferentPackage.getName();
}
rulesCorrect = isEfferentsValid(layer, rules, rulesCorrect, jPackage, analyzedPackageName);
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
}
return rulesCorrect;
}
|
isLayeringValid() メソッドの目的は、リスト 2 の DIRECTORY_TO_ANALYZE ディレクトリーにあるすべてのパッケージの求心性結合を判断することです。リストの下の方を見るとわかるように、このメソッドはリスト 5 に示す isEfferentsValid() メソッドに従います。
isEfferentsValid() メソッドは、パッケージが規定のパッケージ依存関係に従っていないことを検出すると (パッケージ間にゼロより大きい遠心性結合がある場合)、リスト 2 の dataLayerViolations コレクションを使って、そのパッケージにアーキテクチャー違反のフラグを立てます。ちなみに、これによって間接的に testDataLayer() テスト・ケース (リスト 3) が失敗することになります。
リスト 5. パッケージ依存関係違反の判断
private boolean isEfferentsValid(String layer, Collection rules,
boolean rulesCorrect, JavaPackage jPackage, String analyzedPackageName) {
Collection efferents = jPackage.getEfferents();
Iterator efferentItor = efferents.iterator();
while (efferentItor.hasNext()) {
JavaPackage efferentPackage = (JavaPackage) efferentItor.next();
String efferentPackageName = efferentPackage.getName();
for (Iterator it = rules.iterator(); it.hasNext();) {
String value = (String) it.next();
if (analyzedPackageName.equals(layer)
& efferentPackageName.equals(value)) {
rulesCorrect = false;
System.out.println("TEST FAILURE: "
+ analyzedPackageName
+ " should not depend upon (have an efferent coupling to) "
+ efferentPackageName);
break;
}
}
}
return rulesCorrect;
}
|
ご覧のように、リスト 2 からリスト 5 までのコードが協調して原則的に一連のパッケージをスキャンし、結合の変更を調べます。変更があった場合は失敗条件がトリガーされ、それによって JUnit が失敗をレポートするというわけです。私に言わせれば、かなり見事な仕組みだと思います。
テストを自動的に実行するのもお忘れなく
このように JDepend と連動するJUnit 風の制約ベースのテストを作成したら、次に Ant や Maven などのツールを使って、このテストをビルド・プロセスの一部として実行することができます。例えばリスト 6 に示しているのは、Ant を使ってこれらのテストを実行する方法です。test.dependency.dir プロパティーのマッピング先となっている my root/src/test/java/dependency ディレクトリーに、魔法のアーキテクチャーの検証機構が含まれています。
リスト 6. 従属関係制約テストを実行する Ant スクリプト
<target name="run-tests" depends="compile-tests">
<mkdir dir="${logs.junit.dir}" />
<junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
<classpath refid="test.class.path" />
<classpath refid="project.class.path"/>
<formatter type="plain" usefile="true" />
<formatter type="xml" usefile="true" />
<batchtest fork="yes" todir="${logs.junit.dir}">
<fileset dir="${test.dependency.dir}">
<patternset refid="test.sources.pattern"/>
</fileset>
</batchtest>
</junit>
</target>
|
JUnit テストを正常に実行させるためには、Ant のクラスパスに JDepend JAR が含まれている必要があります。haltonfailure 属性は true に設定されているため、テストが失敗した場合にもビルドが停止します。
しきい値主導のアーキテクチャー
前述したように、アーキテクチャーに順守させるための受動的手法には相当な作業が伴わざるを得ません。それに加え、この手法では違反を見逃してしまうのも実に容易なことだと納得していただけたら本望です。アーキテクチャーのテストをビルド・プロセスの一部として実行すれば、このような類のチェックを自動的に繰り返し行うことができます。Ant を実行した後は、図 3 のようなビルドの失敗が表示されるので、まさに理想的です。気が向かなければ、JDepend レポートを見る必要すらありません。
図 3. アーキテクチャー違反によるビルドの失敗
このような事前監視の素晴しいところは、アーキテクチャー階層化の問題が見つかったら、即座に修正できるという点です。問題が発生してから修正するまでの時間が短ければ短いほど、コストは言うまでもなく、リスクも少なくなります。実質的に、チームはタイミングを逃すことなく、実動ソフトウェアを短時間でリリースすることに専心できます
アーキテクチャーのためのオートメーション:
 |
JDepend の魔法を利用する他の手段
JDepend による事前チェックは何通りかの方法で追加できます。JDepend が推奨しているのは、その DependencyConstraint クラスを使用することです。DependencyConstraint を使用すると実装が大幅に簡単になりますが、この記事では使わないことにしました。なぜなら、DependencyConstraint で説明できるのは、その API を使用してアーキテクチャー・ルールを施行する手法に限られ、また、私の要件ではこのクラスが確実には動作しなかったからです。パッケージ依存関係順守をサポートするツールは他にもあるので、「参考文献」で詳細を調べてください。
|
|
これで、ビルド・プロセスを使って、意図するアーキテクチャーに対する設計違反を事前に検出できるようになったはずです。さらに、いくつか考えられる例のうちの 1 つも紹介しました。考えを発展させて、アーキテクチャーの全体的な堅牢性を判断しやすくするために、パッケージの不安定性などの指標を分析することもできるはずです。
ここで概説した手法は、アーキテクチャーに従っているかどうかを判断するために、頻繁にコードのリバース・エンジニアリングを行ってダイアグラムを分析しなくても済むようにするための簡単な方法です。継続的インテグレーション・システムを使用しているのであれば、毎回変更を適用する際に、バージョン管理システムにチェックインするコードがアーキテクチャー・ルールに従っていることを確実にするためのセーフティー・ネットとして今回紹介したテストを使うこともできます。アーキテクチャーに変更を加える場合は、JUnit テストのルールを変更するだけで、チームが確実にプロジェクト標準に従うようにすることができます。これこそ私が、事前の策を講じることによるアーキテクチャー健全性のアサーションと呼んでいるものです。
参考文献 学ぶために
製品や技術を入手するために
-
Ant: Ant を使って Java ビルドを実行してください。
-
JDepend: パッケージに依存関係を分析する JDepend をダウンロードしてください。
-
Macker: ビルド時にアーキテクチャー・ルールをチェックするには、Macker をダウンロードしてください。
-
Japan: XML 構成を使用してパッケージ依存関係を分析するための Ant および IntelliJ プラグインが用意されています。
議論するために
著者について
記事の評価
|