1. 概要

この記事では、ArchUnitを使用してシステムのアーキテクチャを確認する方法を示します。

2. ArchUnitとは何ですか?

アーキテクチャの特性と保守性の間のリンクは、ソフトウェア業界でよく研究されているトピックです。 ただし、システムのサウンドアーキテクチャを定義するだけでは不十分です。 実装されたコードがそれに準拠していることを確認する必要があります。

簡単に言えば、ArchUnitは、アプリケーションが特定のアーキテクチャルールのセットに準拠していることを確認できるテストライブラリです。 しかし、アーキテクチャのルールとは何ですか? さらに、このコンテキストでのアーキテクチャとはどういう意味ですか?

後者から始めましょう。 ここでは、アーキテクチャという用語を使用して、アプリケーション内のさまざまなクラスをパッケージに編成する方法を指します。

システムのアーキテクチャーは、パッケージまたはパッケージのグループ(レイヤーとも呼ばれます)がどのように相互作用するかも定義します。 より実用的な用語では、特定のパッケージのコードが別のパッケージに属するクラスのメソッドを呼び出すことができるかどうかを定義します。 たとえば、アプリケーションのアーキテクチャに、プレゼンテーションサービス、および永続性の3つのレイヤーが含まれているとします。

これらのレイヤーがどのように相互作用するかを視覚化する1つの方法は、各レイヤーを表すパッケージを含むUMLパッケージ図を使用することです。

この図を見るだけで、いくつかのルールを理解できます。

  • プレゼンテーションクラスは、サービスクラスのみに依存する必要があります
  • サービスクラスは永続性クラスのみに依存する必要があります
  • 永続性クラスは他の誰かに依存すべきではありません

これらのルールを見て、元の質問に戻って答えることができます。 このコンテキストでは、アーキテクチャルールは、アプリケーションクラスが相互に作用する方法に関するアサーションです。

では、実装がこれらのルールを遵守していることをどのように確認しますか? ここでArchUnitが登場します。 流暢なAPIを使用してアーキテクチャ上の制約を表現し、通常のビルド中に他のテストと一緒に検証することができます。

3. ArchUnitプロジェクトのセットアップ

ArchUnitJUnitテストフレームワークとうまく統合されるため、通常は一緒に使用されます。  archunit-junit4 依存関係を追加して、JUnitバージョンと一致させるだけです。

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

そのartifactIdが示すように、この依存関係は JUnit4フレームワークに固有です。

JUnit 5を使用している場合は、archunit-junit5依存関係もあります。

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

4. ArchUnitテストの作成

プロジェクトに適切な依存関係を追加したら、アーキテクチャテストの作成を開始しましょう。 テストアプリケーションは、Smurfsをクエリする単純なSpringBootRESTアプリケーションになります。 簡単にするために、このテストアプリケーションには、 Controller Service、、およびRepositoryクラスのみが含まれています。

このアプリケーションが前述のルールに準拠していることを確認したいと思います。 それでは、「プレゼンテーションクラスはサービスクラスのみに依存する必要がある」というルールの簡単なテストから始めましょう。

4.1. 私たちの最初のテスト

最初のステップは、ルール違反がないかチェックされるJavaクラスのセットを作成することです。 これを行うには、 ClassFileImporter クラスをインスタンス化し、その importXXX()メソッドの1つを使用します。

JavaClasses jc = new ClassFileImporter()
  .importPackages("com.baeldung.archunit.smurfs");

この場合、 JavaClasses インスタンスには、メインアプリケーションパッケージとそのサブパッケージのすべてのクラスが含まれています。 このオブジェクトは、ルール評価の対象となるため、通常の単体テストで使用される一般的なテスト対象に類似していると考えることができます。

アーキテクチャルールは、 ArchRuleDefinition クラスの静的メソッドの1つを、流暢なAPI呼び出しの開始点として使用します。 このAPIを使用して、上記で定義した最初のルールを実装してみましょう。 classes()メソッドをアンカーとして使用し、そこから制約を追加します。

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..");
r1.check(jc);

チェックを実行するために作成したルールのcheck()メソッドを呼び出す必要があることに注意してください。 このメソッドはJavaClassesオブジェクトを受け取り、違反がある場合は例外をスローします。

これはすべて良さそうですが、コードに対して実行しようとすると、エラーのリストが表示されます。

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - 
  Rule 'classes that reside in a package '..presentation..' should only 
  depend on classes that reside in a package '..service..'' was violated (6 times):
... error list omitted

なんで? このルールの主な問題はonlyDependsOnClassesThat()です。パッケージ図に記載した内容にもかかわらず、実際の実装はJVMおよびSpringフレームワーククラスに依存しています。したがって、エラーが発生します。

4.2. 最初のテストを書き直す

このエラーを解決する1つの方法は、これらの追加の依存関係を考慮した句を追加することです。

ArchRule r1 = classes()
  .that().resideInAPackage("..presentation..")
  .should().onlyDependOnClassesThat()
  .resideInAPackage("..service..", "java..", "javax..", "org.springframework..");

この変更により、チェックが失敗しなくなります。 ただし、このアプローチには保守性の問題があり、少しハッキーな感じがします。 noClasses()静的メソッドを開始点として使用して、ルールを書き直すこれらの問題を回避できます。

ArchRule r1 = noClasses()
  .that().resideInAPackage("..presentation..")
  .should().dependOnClassesThat()
  .resideInAPackage("..persistence..");

もちろん、このアプローチは、以前の許可ベースではなく拒否ベースであることも指摘できます。重要なポイントは、どのアプローチを選択しても、 ArchUnitは通常、ルールを表現するのに十分な柔軟性があります

5. ライブラリAPIを使用する

ArchUnit は、組み込みのルールのおかげで、複雑なアーキテクチャルールの作成を簡単なタスクにします。 これらを組み合わせて、より高いレベルの抽象化を使用してルールを作成することもできます。 箱から出して、ArchUnitはライブラリAPIを提供します。これは、一般的なアーキテクチャの懸念に対処するパッケージ済みのルールのコレクションです

  • アーキテクチャ:レイヤードとオニオンのサポート(別名 六角形または「ポートとアダプター」)アーキテクチャのルールチェック
  • スライス:循環依存、つまり「サイクル」を検出するために使用されます
  • General :ロギング、例外の使用など、コーディングのベストプラクティスに関連するルールのコレクション。
  • PlantUML :コードベースが特定のUMLモデルに準拠しているかどうかを確認します
  • フリーズアーチルール:後で使用するために違反を保存し、新しい違反のみを報告できるようにします。 技術的負債の管理に特に役立ちます

これらすべてのルールを網羅することは、この紹介の範囲外ですが、Architectureルールパッケージを見てみましょう。 特に、階層化アーキテクチャのルールを使用して、前のセクションのルールを書き直してみましょう。 これらのルールを使用するには、2つのステップが必要です。最初に、アプリケーションのレイヤーを定義します。 次に、許可されるレイヤーアクセスを定義します。

LayeredArchitecture arch = layeredArchitecture()
   // Define layers
  .layer("Presentation").definedBy("..presentation..")
  .layer("Service").definedBy("..service..")
  .layer("Persistence").definedBy("..persistence..")
  // Add constraints
  .whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
  .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
  .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
arch.check(jc);

ここで、 layeredArchitecture()は、Architecturesクラスの静的メソッドです。 呼び出されると、新しい LayeredArchitecture オブジェクトが返されます。このオブジェクトを使用して、名前レイヤーとその依存関係に関するアサーションを定義します。 このオブジェクトは、 ArchRule インターフェイスを実装しているため、他のルールと同じように使用できます。

この特定のAPIの優れた点は、数行のコードルールで作成できることです。そうしないと、複数の個別のルールを組み合わせる必要があります。

6. 結論

この記事では、プロジェクトでArchUnitを使用するための基本について説明しました。 このツールの採用は比較的単純な作業であり、全体的な品質にプラスの影響を与え、長期的には保守コストを削減できます。

いつものように、すべてのコードはGitHubを介して利用できます。