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

ArchUnit で Java / Kotlin アプリケーションのアーキテクチャを CI する / How to check and improve the Java-based application architecture with “ArchUnit” / JJUG CCC 2019 Spring

ArchUnit で Java / Kotlin アプリケーションのアーキテクチャを CI する / How to check and improve the Java-based application architecture with “ArchUnit” / JJUG CCC 2019 Spring

JJUG CCC 2019 Spring(http://www.java-users.jp/ccc2019spring) の登壇資料

当日のタイムライン
https://twitter.com/hashtag/ccc_i2b

登壇レポートブログ
https://tech-blog.rakus.co.jp/entry/20190524/jjug/it-event

以下、プロポーザル
----
ArchUnit とは一言でいうと、Java / Kotlin アプリケーションのパッケージやクラスの依存関係や、プロダクト固有の実装ルールを JUnit のテストコードして表現しテストできるテストフレームワークです。

我々にとって ArchUnit はなにが嬉しいのか。ArchUnit でできることは人間によるコードレビューや静的解析によってもチェックできそうに思えます。

この発表では、実際に私たちのプロダクトの CI に組み込まれているテストコードの実例と合わせて、アーキテクトの観点、開発チームの観点から ArchUnit のウレシみについてお話します。

Yu Kawanami

May 18, 2019
Tweet

More Decks by Yu Kawanami

Other Decks in Technology

Transcript

  1. ArchUnit で Java / Kotlin アプリケーションの アーキテクチャを CI する JJUG

    CCC 2019 Spring @kawanamiyuu
  2. How to check and improve the Java-based application architecture with

    “ArchUnit” JJUG CCC 2019 Spring @kawanamiyuu
  3. 自己紹介 • かわなみゆう • @kawanamiyuu • 株式会社ラクス / Lead Engineer

    • 新規事業 ( B2B SaaS x HR Tech ) の開発 • Spring Boot / Doma2 / Flyway / Vue.js • Java に恋する PHPer 3
  4. 【宣伝】今日の裏セッションが Meetup で再演されます! 4 https://rakus.connpass.com/event/125505/

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

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

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

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

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

    ソースコードの品質担保以上に、アーキテクチャの品質担保は難しい
  10. それ解決できます!ArchUnit ならね! 10

  11. 11 https://www.archunit.org/

  12. 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
  13. ArchUnit を一言でいうと • Java / Kotlin アプリケーションのパッケージやクラスの依存 関係を JUnit のテストコードとして表現し、テストできるテストフ

    レームワーク • 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる 13
  14. ArchUnit との出会い • 「進化的アーキテクチャ」という書籍を読んだ • 適応度関数(※)の 1 つとして、Java クラスの依存関係をテス トできる

    JDepend が紹介されていた • 類似ツールを探してみたところで ArchUnit を発見した ※適応度関数:システムのアーキテクチャ特性がどれだけ要件を満たしているかを評価 する指標、手段 14
  15. ArchUnit のなにが嬉しいのか? 15

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

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

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

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

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

  21. 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); }
  22. 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); }
  23. 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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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)
  24. 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): // 事頁に続く
  25. 25 // 前頁の続き(見やすさのため着色) Constructor <com.example.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type

    <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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)
  26. 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); }
  27. 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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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)
  28. 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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositor yImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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)
  29. わたしたちのプロダクトでの事例 29

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

  31. 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); }
  32. 事例2 あなたの common パッケージは 本当に common? 32

  33. 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); }
  34. 事例3 アプリケーション固有の 実装ルールをチェックせよ! 34

  35. 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); }
  36. 事例4 実装ミスによる不具合の再発 を防止せよ! 36

  37. 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); }
  38. まとめ 38

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

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

  41. thank you ! Let’s check and improve the Java-based application

    architecture with “ArchUnit”
  42. 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