RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

ドメイン駆動設計を支えるアーキテクチャテスト

@kawanamiyuu です。この記事は「ドメイン駆動設計#1 Advent Calendar 2019」の 6 日目の記事です。

1. はじめに

最近、技術イベントやまた社内でも「ドメイン駆動設計」や「クリーンアーキテクチャ」についての話題をこれまでにも増してよく耳にするようになりました。

私も楽楽労務という実際のプロダクト開発で 1 年以上、ドメイン駆動設計に取り組んできました。ドメイン駆動設計 チョットデキル ような気がしてきたころ、このように思いました。

ドメイン駆動設計って、『オブジェクト指向設計をちゃんとやる』ってことだよね?」

ドメイン駆動設計もクリーンアーキテクチャも、オブジェクト指向設計原則のうえに成り立つ設計プラクティスであり、ただ、重要な視点として、オブジェクト指向設計原則の適用を検討する対象がクラスだけではありません。もう少し大きな単位、パッケージ・モジュール・レイヤー、まで視野を広げます。

  • 依存関係が注意深く設計され、単一の責任を持ったモジュールやパッケージが、ドメインをかたちづくります
  • レイヤーやパッケージの依存関係の逆転により、ドメインは自身の関心や責務を逸脱することなくビジネスロジックの実現に集中できます

ドメイン駆動設計がオブジェクト指向プログラミングによって実現されるということは、そのアーキテクチャも、私たちが慣れたいつもの方法 ─ ユニットテスト ─ で、テストすることができます。ソースコードアーキテクチャも、一度つくって終わりではありません。プロダクト開発が続くかぎり、常に見直され改善されるべき対象です。

......

前置きが長くなりましたが、この記事では依存関係という切り口から、アーキテクチャの品質を継続的に維持・改善していくための手法として、ArchUnit (Java) によるアーキテクチャテストを紹介します。

github.com

ArchUnit って何?については昨年のアドベントカレンダーに投稿しています。

qiita.com

2. ドメインの依存関係に対するアーキテクチャテスト

今回は、私が開発を担当している人事労務管理システムを例にします。人事労務管理システムとは文字通り、企業の人事労務担当者の業務をにするための業務システムです。

このシステムが扱う業務の 1 つである、「従業員の入社関連業務」は以下のようなものです。

  1. 会社に従業員が入社したときに、従業員情報(氏名や住所といった個人情報、基礎年金番号といった社会保険に関する情報、など)を収集する
  2. 入社後、社内申請フローにより追加の個人情報が収集され、役所への社会保険等の提出書類(届出書という)を作成するための行政手続きフローが開始される
  3. 行政手続きフローを進めることで、(複数の)届出書が生成される

2.1. 人事労務管理システムのドメイン

業務ドメインの境界が比較的わかりやすく、大きく 4 つに分けられます。

これらのドメインJava プロジェクトのパッケージとして表現すると以下のようになります。

f:id:kawanamiyuu:20191204211948p:plain:w300

2.2. ドメインの依存関係

ドメイン同士の依存関係は次のとおりです。

  • 「従業員ドメイン」(いわゆる従業員マスタを含む)は、人事労務管理システムという特性上あらゆるドメインから参照されます
  • 「社内申請ドメイン」は、後続の業務であるの「行政手続き」を知っています
  • 「行政手続きドメイン」は、その手続きで必要となる「届出書」を知っています

f:id:kawanamiyuu:20191204230011p:plain:w500

2.3. ドメインの依存関係に対するアーキテクチャテスト

ドメインの依存関係に対するアーキテクチャテスト(以下の例は一部)は ArchUnit で次のように実装できます。

private static final JavaClasses CLASSES =
        new ClassFileImporter()
                .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                .importPackages("com.example");

@Test
void 従業員ドメインは他のドメインに依存しない() {
    noClasses().that().resideInAPackage("com.example.domain.employee..")
            .should()
            .dependOnClassesThat(new DescribedPredicate<>("従業員ドメイン以外のドメイン") {
                @Override
                public boolean apply(JavaClass input) {
                    if (! input.getPackageName().startsWith("com.example")) {
                        // プロジェクト外(サードパーティライブラリ等)への依存はOK
                        return false;
                    }

                    return ! input.getPackageName().startsWith("com.example.domain.employee");
                }
            })
            .check(CLASSES);
}

@Test
void 社内申請ドメインは届出書ドメインに依存しない() {
    noClasses().that().resideInAPackage("com.example.domain.request..")
            .should()
            .dependOnClassesThat().resideInAPackage("com.example.domain.report..")
            .check(CLASSES);
}

@Test
void 行政手続きドメインは社内申請ドメインからのみ依存される() {
    classes().that().resideInAPackage("com.example.domain.procedure..")
            .should()
            .onlyBeAccessed().byAnyPackage("com.example.domain.request..")
            .check(CLASSES);
}

3. レイヤーの依存関係に対するアーキテクチャテスト

ドメイン駆動設計の文脈でよく登場するアーキテクチャとして、「レイヤーアーキテクチャ」があります。ArchUnit でレイヤーアーキテクチャに対するテストは以下のように実装できます。

※この記事では省略しますが、オニオンアーキテクチャ(ヘキサゴナルアーキテクチャ)に対するテストも ArchUnit で実装できます。興味のある方は User Guide を参照ください。

3.1. シンプルなレイヤーアーキテクチャ

上位の層が下位の層に依存します。

f:id:kawanamiyuu:20191204090626p:plain:w300

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

3.2. 依存関係を逆転したレイヤーアーキテクチャ

ドメイン層とインフラストラクチャ層の依存関係(依存の方向)を逆転させることで、ドメイン層はどの層にも依存せず、ドメイン層に閉じてビジネスロジックの実現に専念できます。

f:id:kawanamiyuu:20191204091012p:plain:w300

@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").mayOnlyBeAccessedByLayers("infra")
        .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui")
        .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app")
        .whereLayer("infra").mayNotBeAccessedByAnyLayer()

        .check(CLASSES);
}

4. さいごに

  • ドメイン駆動設計とは、特別な銀の弾丸的な設計手法ではなく、オブジェクト指向設計という基本的かつ重要なプログラミング原則のうえに成り立つ設計プラクティスです
  • クラスだけではなく、レイヤーやモジュール・パッケージといった粒度でも依存関係を注意深く設計し、依存関係を管理していくことが重要で、
  • ソースコードアーキテクチャも一度つくって終わりではなく、常に見直され改善されるべき対象であり、アーキテクチャも継続的にテストすることができます

ドメイン駆動設計、やっていきましょう!

......

tech-blog.rakus.co.jp

Copyright © RAKUS Co., Ltd. All rights reserved.