Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実践ArchUnit ~実例による検証パターンの紹介~

実践ArchUnit ~実例による検証パターンの紹介~

Avatar for 荻原利雄

荻原利雄

June 07, 2025
Tweet

More Decks by 荻原利雄

Other Decks in Programming

Transcript

  1. 2 荻原 利雄(オギワラ トシオ) • 所属 / 職種 - 株式会社豆蔵

    - ビジネスソリューション事業部 - 主幹ソフトウェアエンジニア • プロフィール - オブジェクト指向とともにエンタープライズ なJavaアプリを作りつづけて25年のアラフィ フエンジニア - ここ数年は大規模基幹システムを支える JakartaEEフルスタックなフレームワークや Spring Bootを使った共通機能の開発を行って いる extact-io 豆蔵デベロッパーサイト 豆蔵デベロッパーサイトで色々執筆中! toshio-ogiwara
  2. 本日の説明は • ArchUnitを使うとこんなことができますよ!の紹介が主な目的となります • なので、ArchUnitの細かい使い方やAPIの詳細は説明しません • 説明しませんが、文のように読める宣言的なAPIなので雰囲気で理解していただけると思 います • 興味をもっていただけた方は、ArchUnitの公式ページのリファレンスを是非み

    てみてください。とても分かりやすく説明されているのでお勧めです • スライドの説明に使った完全なサンプルは動作可能な状態で一式GitHubにアッ プしています。スライドでは紹介できなった例も沢山入っています 4 ←サンプルのアクセスはこちらから https://github.com/extact-io/jjug-ccc-2025-spring
  3. ArchUnitとは • Javaアプリケーションのアーキテクチャルール(設計規約)をコー ドで定義し、JUnitで検証できるライブラリ 6 @AnalyzeClasses(packages = "com.mamezou.sample", …) class

    EmployeeApplicationArchUnitTest { /** * webapiパッケージ配下でRmsRestControllerアノテーションが付いているクラスの * サフィックスは"Controller"となっていること。 */ @ArchTest static final ArchRule naming_controller_should_be_suffixed = classes() .that() .resideInAPackage("..webapi..").and() .areAnnotatedWith(RestController.class) .should().haveSimpleNameEndingWith("Controller"); … こんな感じルールを実装 普通のJUnitのテストク ラスと同じようにJUnit ランナーから実行できる
  4. アーキテクチャスタイル準拠の確認 • まずは全体の構造であるアーキテクチャスタイルを確認する • ArchUnitが直接にサポートするスタイルは以下の2つ • Layered Architecture • Onion

    Architecture • この2つはそれぞれのスタイルに特化したルール定義APIが用意 されている • Hexagonal(Ports and Adapters) Architectureなど、これ以外のスタ イルを定義できないわけではなく汎用的なルールAPIを組み合わせるこ とで定義は可能 8 今回はこちらをベースに紹介していきます
  5. Onion Architecture とは 9 adapter (interface) adapter (infra) applicationService domianService

    domainModel https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ flattened • 円の中心方向に依存させていく • 円の外側への依存は認めない • 外部環境からの影響を受けないようにし てドメインをクリーンに保つ • adapter同士の依存はNG • adapterはそれぞれ独立 していること domain • 下位レイヤすべてに依存 してOK(open layer) • なのでルール上はdomain modelをinterfaceで扱う ことも許容されている Hexagonalはapapterから domainの要素を直接参照す ることは許容されない。 Onionとはこの点が異なる
  6. Onion Architectureのルールを定義する 11 @AnalyzeClasses(packages = "com.mamezou.sample", …) class EmployeeApplicationArchUnitTest {

    … packages属性に起点となる パッケージを指定する 「オニオンアーキテクチャです」と宣言した後に 「domainMolesのパッケージは◦◦です」といっ たようにアーキテクチャ要素とパッケージの対応 を宣言してく @ArchTest static final ArchRule architecture_respect_onion = onionArchitecture() .domainModels("..domain.model..") .domainServices( "..domain.service..", "..domain.repository..") .applicationServices("..application..") .adapter("interface", "..webapi..") .adapter("infrastructure", "..infrastructure..");
  7. Onion Architectureのルール違反があると 12 java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule

    'Onion architecture consisting of domain models ('..domain.model..') domain services ('..domain.service..', '..domain.repository..') application services ('..application..') adapter 'interface' ('..webapi..') adapter 'infrastructure' ('..infrastructure..')' was violated (1 times): Field <com.mamezou.sample.webapi.admin.AdminUserController.repository> has type <com.mamezou.sample.infrastructure.file.EmployeeFileRepository> in (AdminUserController.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:86) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:347) at com.tngtech.archunit.library.Architectures$OnionArchitecture.check(Architectures.java:1039) at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:168) at com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor$ArchUnitRuleDescriptor.execute(ArchUnitTestDescriptor.java:151) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
  8. レイヤごとに不適切な依存がないか確認する • レイヤごとに依存してよいライブラリは異なる。よって、レイ ヤごとに許容しているライブラリをルートして定義する 16 @ArchTest static final ArchRule dependency_webapi

    = classes() .that() .resideInAPackage(“..webapi..”) // webapiパッケージ内のクラスは .should().onlyDependOnClassesThat() // 指定されたクラスにのみ依存すべき .resideInAnyPackage( // その指定は次にあげるパッケージのクラス "java..", "lombok..", "org.springframework.web..", // Spring MVCには依存してOK "org.springframework.http..", // Spring MVCには依存してOK "..domain..", "..application..", "..webapi.." // ); webapiパッケージ内でここに示したパッケージ以外のクラスに 対して依存がある場合はエラーとなる <interface(webapi)レイヤの場合>
  9. レイヤごとに不適切な依存がないか確認する 17 @ArchTest static final ArchRule dependency_application = classes() .that()

    .resideInAPackage("..application..") .should().onlyDependOnClassesThat( resideInAnyPackage( "java..", "lombok..", "..domain..", "..application..") .or(type(org.springframework.transaction.annotation.Transactional.class)) .or(type(org.springframework.transaction.annotation.Isolation.class)) .or(type(org.springframework.transaction.annotation.Propagation.class)) ); ApplicationServiceは原則Springの依存 はさけるべき。 依存せざる得ない場合はパッケージ単位 ではなくクラス単位での指定もできる <ApplicationServiceレイヤの場合>
  10. クラス間の関係により決定するネーミングルール が守られているか確認する • 「このインターフェースを実装クラスは」や「このアノテーション を付けるクラスは」といったクラス間の関係により決定するネーミ ングがある場合は、その関係をネーミングルールとして定義する 19 @ArchTest static final

    ArchRule naming_controller_should_be_suffixed = classes() .that() .resideInAPackage("..webapi..") .and().areAnnotatedWith(RestController.class) .should().haveSimpleNameEndingWith("Controller"); webapiパッケージ配下でRestController アノテーションが付いているクラス名の サフィックスは"Controller"となってい ること @ArchTest static final ArchRule naming_controller_should_be_suffixed_reverse = classes() .that() .resideInAPackage("..webapi..") .and().haveSimpleNameEndingWith("Controller") .should().beAnnotatedWith(RestController.class); RestControllerアノテーションが付いて いないのにクラス名のサフィックスが “Controller”となってものがないこと 必要な場合は反対側の条件でもチェック
  11. クラス間の関係により決定するネーミングルール が守られているか確認する 20 @ArchTest static final ArchRule naming_idenity_should_be_suffixed = classes()

    .that() .resideInAPackage("..domain..") .and().implement(Identity.class) // Identityインターフェースの実装クラスは .should().haveSimpleNameEndingWith(“Id”); // クラス名が“Id”で終わっていること @ArchTest static final ArchRule naming_idenity_should_be_suffixed_reverse = classes() .that() .resideInAPackage("..domain.model..") .and().haveSimpleNameEndingWith(“Id”) // クラス名が“Id”で終わっているクラスは .should().beAssignableTo(Identity.class); // Identityインターフェースを実装していること 必要な場合は反対側の条件でもチェック <インターフェースの実装関係の例>
  12. まとめ • ArchUnitを使う前は定義したアーキテクチャルールが守られて いるかの確認はレビューやコードのgrep検索など属人的な作業 になりがちでした • ArchUnitでアーキテクチャルールを実装することで自動化する ことができ、そしてなによりもその精度を格段に向上させるこ とができます •

    すべてのアーキテクチャルールをArchUnitで実装できるわけで はなく、できるのは構造や依存関係といった静的な側面だけと なりますが、その効果には大きいものがあります • ArchUnitには今回紹介した以外にも沢山の条件メソッドが用意 されています。これを機会に活用いただければ幸いです 22
  13. 【オマケ】応用例の紹介-ルール定義 25 @ArchTest static final ArchRule architecture_respect_addon_rule_for_onion = noClasses() .that()

    .resideInAPackage("..webapi..") .should().dependOnClassesThat( resideInAnyPackage("..domain..") // ValueObjectインターフェースの実装クラス .and(not(implement(ValueObject.class))) // EntityModelViewのサブインターフェース .and(not(subInterface(EntityModelView.class))) );