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

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

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. How to check and improve the Java-based application architecture with

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

    • 新規事業 ( B2B SaaS x HR Tech ) の開発 • Spring Boot / Doma2 / Flyway / Vue.js • Java に恋する PHPer 3
  3. 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
  4. ArchUnit を一言でいうと • Java / Kotlin アプリケーションのパッケージやクラスの依存 関係を JUnit のテストコードとして表現し、テストできるテストフ

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

    JDepend が紹介されていた • 類似ツールを探してみたところで ArchUnit を発見した ※適応度関数:システムのアーキテクチャ特性がどれだけ要件を満たしているかを評価 する指標、手段 14
  6. 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); }
  7. 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); }
  8. 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)
  9. 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): // 事頁に続く
  10. 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)
  11. 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); }
  12. 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)
  13. 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)
  14. 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); }
  15. 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); }
  16. 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); }
  17. 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); }
  18. 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