Slide 1

Slide 1 text

実践ArchUnit ~実例による検証パターンの紹介~ 株式会社 豆蔵 ビジネスソリューション事業部 主幹ソフトウェアエンジニア 荻原 利雄 2025/6/7 JJUG CCC 2025 Spring

Slide 2

Slide 2 text

2 荻原 利雄(オギワラ トシオ) • 所属 / 職種 - 株式会社豆蔵 - ビジネスソリューション事業部 - 主幹ソフトウェアエンジニア • プロフィール - オブジェクト指向とともにエンタープライズ なJavaアプリを作りつづけて25年のアラフィ フエンジニア - ここ数年は大規模基幹システムを支える JakartaEEフルスタックなフレームワークや Spring Bootを使った共通機能の開発を行って いる extact-io 豆蔵デベロッパーサイト 豆蔵デベロッパーサイトで色々執筆中! toshio-ogiwara

Slide 3

Slide 3 text

Agenda 1. ArchUnitの超概要 2. ソフトウェア構造を確認しよう 3. モジュールの依存関係を確認しよう 4. クラス名を確認しよう 5. まとめ 3

Slide 4

Slide 4 text

本日の説明は • ArchUnitを使うとこんなことができますよ!の紹介が主な目的となります • なので、ArchUnitの細かい使い方やAPIの詳細は説明しません • 説明しませんが、文のように読める宣言的なAPIなので雰囲気で理解していただけると思 います • 興味をもっていただけた方は、ArchUnitの公式ページのリファレンスを是非み てみてください。とても分かりやすく説明されているのでお勧めです • スライドの説明に使った完全なサンプルは動作可能な状態で一式GitHubにアッ プしています。スライドでは紹介できなった例も沢山入っています 4 ←サンプルのアクセスはこちらから https://github.com/extact-io/jjug-ccc-2025-spring

Slide 5

Slide 5 text

5 1. ArchUnitの超概要

Slide 6

Slide 6 text

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 ランナーから実行できる

Slide 7

Slide 7 text

7 2. ソフトウェア構造を確認しよう

Slide 8

Slide 8 text

アーキテクチャスタイル準拠の確認 • まずは全体の構造であるアーキテクチャスタイルを確認する • ArchUnitが直接にサポートするスタイルは以下の2つ • Layered Architecture • Onion Architecture • この2つはそれぞれのスタイルに特化したルール定義APIが用意 されている • Hexagonal(Ports and Adapters) Architectureなど、これ以外のスタ イルを定義できないわけではなく汎用的なルールAPIを組み合わせるこ とで定義は可能 8 今回はこちらをベースに紹介していきます

Slide 9

Slide 9 text

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とはこの点が異なる

Slide 10

Slide 10 text

Onion Architectureのサンプル 10 adapter (interface) adapter (infra) applicationService domianService domainModel 準拠させた <ルール> <今回のサンプル>

Slide 11

Slide 11 text

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..");

Slide 12

Slide 12 text

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 has type 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)

Slide 13

Slide 13 text

モジュールの独立性を確認する • 実装を切り替えて利用することを想定している場合はそれぞれのモ ジュールがお互いに独立していることを確認する 13 /** * infrastructure配下のパッケージ(database/file/inmemory)は独立し相互に依存していないこと。 * 例)databaseパッケージがfileパッケージを利用しているといったことがないこと */ @ArchTest static final ArchRule isolate_infrastructure_not_depend_on_each_other = SlicesRuleDefinition.slices() .matching("..infrastructure.(*)..") .should().notDependOnEachOther(); SlicesのnotDependOnEachOtherを使うと簡単に定義できる

Slide 14

Slide 14 text

14 3. モジュールの依存関係を確認しよう

Slide 15

Slide 15 text

モジュール間に循環参照がないことを確認する • 同列にいるモジュール同士が独立していない場合、気づかぬうちに循環参 照が発生していることがある 15 @ArchTest static final ArchRule isolate_domain_model_be_free_of_cycles = SlicesRuleDefinition.slices() .matching("..domain.model(*)..") .should().beFreeOfCycles(); SlicesのbeFreeOfCyclesを使うと簡単に定義できる

Slide 16

Slide 16 text

レイヤごとに不適切な依存がないか確認する • レイヤごとに依存してよいライブラリは異なる。よって、レイ ヤごとに許容しているライブラリをルートして定義する 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)レイヤの場合>

Slide 17

Slide 17 text

レイヤごとに不適切な依存がないか確認する 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レイヤの場合>

Slide 18

Slide 18 text

18 4. クラス名を確認しよう

Slide 19

Slide 19 text

クラス間の関係により決定するネーミングルール が守られているか確認する • 「このインターフェースを実装クラスは」や「このアノテーション を付けるクラスは」といったクラス間の関係により決定するネーミ ングがある場合は、その関係をネーミングルールとして定義する 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”となってものがないこと 必要な場合は反対側の条件でもチェック

Slide 20

Slide 20 text

クラス間の関係により決定するネーミングルール が守られているか確認する 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インターフェースを実装していること 必要な場合は反対側の条件でもチェック <インターフェースの実装関係の例>

Slide 21

Slide 21 text

21 5. まとめ

Slide 22

Slide 22 text

まとめ • ArchUnitを使う前は定義したアーキテクチャルールが守られて いるかの確認はレビューやコードのgrep検索など属人的な作業 になりがちでした • ArchUnitでアーキテクチャルールを実装することで自動化する ことができ、そしてなによりもその精度を格段に向上させるこ とができます • すべてのアーキテクチャルールをArchUnitで実装できるわけで はなく、できるのは構造や依存関係といった静的な側面だけと なりますが、その効果には大きいものがあります • ArchUnitには今回紹介した以外にも沢山の条件メソッドが用意 されています。これを機会に活用いただければ幸いです 22

Slide 23

Slide 23 text

23 ご清聴ありがとうございました

Slide 24

Slide 24 text

【オマケ】応用例の紹介-やりたいこと 24 オニオンアーキテクチャはdomainレイヤはどのレイ ヤからも依存を許可する開放レイヤースタイルを 採っているが、webapisからdomainに対しては一部の モジュールのアクセスを許容するが、それ以外は webapi→application→domianの閉鎖レイヤースタイ ルとしたい 値オブジェクトは副作 用がないためwebapiで の利用は許可 ドメインモデルは副作用をもった 操作も持っているためwebapiでの 利用は禁止するが、取得操作しか 定義していないModelViewイン ターフェースを通した利用は許可 する

Slide 25

Slide 25 text

【オマケ】応用例の紹介-ルール定義 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))) );