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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

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

    View Slide

  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):
    // 事頁に続く

    View Slide

  25. 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)

    View Slide

  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);
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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);
    }

    View Slide

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

    View Slide

  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);
    }

    View Slide

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

    View Slide

  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);
    }

    View Slide

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

    View Slide

  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);
    }

    View Slide

  38. まとめ
    38

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide