Slide 1

Slide 1 text

ArchUnit で Java / Kotlin アプリケーションの アーキテクチャを CI する JJUG CCC 2019 Spring @kawanamiyuu

Slide 2

Slide 2 text

How to check and improve the Java-based application architecture with “ArchUnit” JJUG CCC 2019 Spring @kawanamiyuu

Slide 3

Slide 3 text

自己紹介 ● かわなみゆう ● @kawanamiyuu ● 株式会社ラクス / Lead Engineer ● 新規事業 ( B2B SaaS x HR Tech ) の開発 ● Spring Boot / Doma2 / Flyway / Vue.js ● Java に恋する PHPer 3

Slide 4

Slide 4 text

【宣伝】今日の裏セッションが Meetup で再演されます! 4 https://rakus.connpass.com/event/125505/

Slide 5

Slide 5 text

こんな悩みありませんか? 5

Slide 6

Slide 6 text

アーキテクトの悩み ● 開発初期に頑張って検討した設計方針が、納期優先・相次ぐ メンバー増員により、いつのまにか泥団子に ● 開発プロセスとしてコードレビューは機能しているが、アーキテ クチャの観点ではレビューされない ● 開発メンバーに設計力を上げてもらうためにチャレンジさせた いけど、丸投げするのはちょっと不安 6

Slide 7

Slide 7 text

開発メンバーの悩み ● どのパッケージにクラスを置いたらいいか毎回迷う ● コードレビューで指摘されたけど、そんなルール聞いていな し、ドキュメントもないので知りようがない ● ドメイン駆動設計?Clean Architecture?難しそうだし、ソー スコードがどうあればそれらが適用されたアーキテクチャとい えるのかイメージできない 7

Slide 8

Slide 8 text

悩みの原因 ● アーキテクチャ設計に関する知識が属人化している ● アーキテクチャ設計に関する知識が暗黙知化している ● 知っている人が人力でチェックするしかない ● 知らなければ当然、チェックされることなくすり抜けてしまう 8

Slide 9

Slide 9 text

悩みの原因 ● アーキテクチャ設計に関する知識が属人化している ● アーキテクチャ設計に関する知識が暗黙知化している ● 知っている人が人力でチェックするしかない ● 知らなければ当然、チェックされることなくすり抜けてしまう 9 ソースコードの品質担保以上に、アーキテクチャの品質担保は難しい

Slide 10

Slide 10 text

それ解決できます!ArchUnit ならね! 10

Slide 11

Slide 11 text

11 https://www.archunit.org/

Slide 12

Slide 12 text

ArchUnit ● GitHub ○ https://github.com/TNG/ArchUnit ○ https://github.com/TNG/ArchUnit-Examples ● Twitter ○ https://twitter.com/archtests ● Technology Radar ○ https://www.thoughtworks.com/radar/tools/archunit ○ 進化的アーキテクチャ x 適応度関数 12

Slide 13

Slide 13 text

ArchUnit を一言でいうと ● Java / Kotlin アプリケーションのパッケージやクラスの依存 関係を JUnit のテストコードとして表現し、テストできるテストフ レームワーク ● 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる 13

Slide 14

Slide 14 text

ArchUnit との出会い ● 「進化的アーキテクチャ」という書籍を読んだ ● 適応度関数(※)の 1 つとして、Java クラスの依存関係をテス トできる JDepend が紹介されていた ● 類似ツールを探してみたところで ArchUnit を発見した ※適応度関数:システムのアーキテクチャ特性がどれだけ要件を満たしているかを評価 する指標、手段 14

Slide 15

Slide 15 text

ArchUnit のなにが嬉しいのか? 15

Slide 16

Slide 16 text

アーキテクチャのレビューの自動化 ● ArchUnit でテストできることは、例えばドキュメントにして周知 したり、コードレビューで指摘したりすることで人力でもカバー することは一応可能 ● ただ、人間がチェックする場合、それらを毎回忘れずにできる かというと難しいし、そもそも知らないとできないので、やはり 自動化できることには価値がある 16

Slide 17

Slide 17 text

アーキテクチャの形式知化 ● アーキテクチャや、アプリケーション固有の実装ルールがコー ド化されていることで、設計に関する暗黙知が形式知になる ● 技術力にばらつきがある開発チームで、一定の強制力をもっ てアーキテクチャの設計品質を担保できる ● レガシーコードで、リファクタリングすべき実装を見つけるのに 役立つ 17

Slide 18

Slide 18 text

アーキテクチャの典型的なテストの例 18

Slide 19

Slide 19 text

アーキテクチャの典型的なテスト ● レイヤーアーキテクチャ ● パッケージ間・クラス間の依存関係 ● 循環参照の禁止 19

Slide 20

Slide 20 text

アーキテクチャの典型的なテスト ● レイヤーアーキテクチャ ● パッケージ間・クラス間の依存関係 ● 循環参照の禁止 20

Slide 21

Slide 21 text

21 private static final JavaClasses CLASSES = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS).importPackages("com.example"); @Test void 一般的なレイヤーアーキテクチャ() { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayNotBeAccessedByAnyLayer() .whereLayer("app").mayOnlyBeAccessedByLayers("ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("ui", "app") .whereLayer("infra").mayOnlyBeAccessedByLayers("ui", "app", "domain") .check(CLASSES); }

Slide 22

Slide 22 text

22 @Test void DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ() { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 23

Slide 23 text

23 $ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ () FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..') layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28)

Slide 24

Slide 24 text

24 // (見やすさのため着色) com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ() FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..') layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): // 事頁に続く

Slide 25

Slide 25 text

25 // 前頁の続き(見やすさのため着色) Constructor (com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28)

Slide 26

Slide 26 text

26 // パッケージ間の依存関係として表現した場合 @Test void UI層のクラスはインフラストラクチャ層のクラスからのみ依存される() { classes().that().resideInAPackage("com.example.presentation..") .should() .onlyHaveDependentClassesThat().resideInAPackage("com.example.infrastructure..") .check(CLASSES); } @Test void ドメイン層のクラスは他の層のクラスに依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAnyPackage( "com.example.presentation..", "com.example.application..", "com.example.infrastructure..") .check(CLASSES); }

Slide 27

Slide 27 text

27 $ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > ドメイン層のクラスは他の層のクラスに依存しない() FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package 'com.example.domain..' should depend on classes that reside in any package ['com.example.presentation..', 'com.example.application..', 'com.example.infrastructure..']' was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:195) at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81) at com.example.ArchitectureTest.ドメイン層のクラスは他の層のクラスに依存しない(ArchitectureTest.java:38)

Slide 28

Slide 28 text

28 $ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > ドメイン層のクラスは他の層のクラスに依存しない() FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package 'com.example.domain..' should depend on classes that reside in any package ['com.example.presentation..', 'com.example.application..', 'com.example.infrastructure..']' was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:195) at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81) at com.example.ArchitectureTest.ドメイン層のクラスは他の層のクラスに依存しない(ArchitectureTest.java:38)

Slide 29

Slide 29 text

わたしたちのプロダクトでの事例 29

Slide 30

Slide 30 text

事例1 ドメイン層を独立させよ! 30

Slide 31

Slide 31 text

31 @Test void ドメイン層はWeb実行環境に依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("javax.servlet..") .check(CLASSES); } @Test void ドメイン層はWebアプリケーションフレームワークに依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("org.springframework..") .check(CLASSES); }

Slide 32

Slide 32 text

事例2 あなたの common パッケージは 本当に common? 32

Slide 33

Slide 33 text

33 @Test void commonパッケージは特定の業務ドメインに依存しない() { noClasses().that().resideInAPackage("com.example.domain.common..") .should() .dependOnClassesThat(new DescribedPredicate<>("common 以外のパッケージ") { @Override public boolean apply(JavaClass input) { if (! input.getPackageName().startsWith("com.example")) { // JDK の標準パッケージなどへの依存はOK return true; } return ! input.getPackageName().startsWith("com.example.domain.common"); } }) .check(CLASSES); }

Slide 34

Slide 34 text

事例3 アプリケーション固有の 実装ルールをチェックせよ! 34

Slide 35

Slide 35 text

35 @Test void APIエンドポイントではユーザの権限を必ずチェックする () { classes().that().areAnnotatedWith(RestController.class) .should(new ArchCondition<>("API のエンドポイントとなるメソッドは @Role でアノテートする ") { @Override public void check(JavaClass input, ConditionEvents events) { input.getMethods().stream() .filter(method -> method.isAnnotatedWith(RequestMapping.class)) .filter(method -> ! method.isAnnotatedWith(Role.class)) .forEach(method -> { // 実装違反を通知 events.add(SimpleConditionEvent.violated(input, input.getName() + "#" + method.getName() + " should be annotated with @Role.")); }); } }) .check(CLASSES); }

Slide 36

Slide 36 text

事例4 実装ミスによる不具合の再発 を防止せよ! 36

Slide 37

Slide 37 text

37 @Test void JSONシリアライズされる列挙はデシリアライズ方法も実装する () { classes() .that(new DescribedPredicate<>("JSON シリアライズ可能な列挙 ") { @Override public boolean apply(JavaClass input) { return input.isEnum() && input.getMethods().stream().anyMatch(method -> method.isAnnotatedWith(JsonValue.class)); } }) .should(new ArchCondition<>("JSON デシリアライズ方法を実装する ") { @Override public void check(JavaClass input, ConditionEvents events) { boolean hasJsonCreator = input.getMethods().stream().anyMatch(method -> method.isAnnotatedWith(JsonCreator.class)); if (! hasJsonCreator) events.add(SimpleConditionEvent.violated(input, input.getName() + " should implement a method annotated with @JsonCreator.")); } }) .check(CLASSES); }

Slide 38

Slide 38 text

まとめ 38

Slide 39

Slide 39 text

まとめ ● レイヤーアーキテクチャのような形の定まったアーキテクチャ のテストから、アプリケーション固有の実装ルールのテストま で、柔軟に活用できる ● 今回紹介した事例以外にもアイデア次第でいろいろな観点の テストが実装できそう 39

Slide 40

Slide 40 text

まとめ ● 自動テストでアーキテクチャの品質を担保しつつ、プロダクト開 発を通じて継続的にアーキテクチャを磨いていくことができると したら... 40

Slide 41

Slide 41 text

thank you ! Let’s check and improve the Java-based application architecture with “ArchUnit”

Slide 42

Slide 42 text

Appendix. 資料中のソースコードの動作環境 ● macOS High Sierra 10.13.6 ● AdoptOpenJDK 11.0.3+7 ● ArchUnit 0.10.2 【書籍】進化的アーキテクチャ ● https://www.oreilly.co.jp/book s/9784873118567/ Add "onion architecture" builder ● https://github.com/TNG/Arch Unit/pull/174 42