Help us understand the problem. What is going on with this article?

DDD のポリシーと静的解析

この記事はラクスアドベントカレンダーの4日目の記事です。
昨日は @mikachiプロダクトマネジメントって? ふんわり入門でした。

はじめに

今年になって新しいプロジェクトに携わることになり「せっかくだからちょっと DDD でも取り入れてみるか」と思い立ちました。
最初は原典たる DDD 本 を読み始めましたが、概念的な内容が多く 100 ページあたりで心が折れました。。
(近々リベンジします)
そこで DDD に関しては双璧と言われる 実践ドメイン駆動設計 (以降 IDDD 本)に切り替えたところ、こちらは性に合っていたらしく完読できました。

というわけでこの記事の内容は IDDD 本をバイブルとして書いています。

新しいプロジェクトに DDD を取り入れるのはいいのですが、これまでやっていたプロジェクトとかなり変わることになるので DDD のポリシーを遵守できるか不安です。
そこで静的解析で DDD のポリシーに違反していないかチェックすることにしました。
以降で IDDD 本で取り上げられているポリシーとそれを遵守するための静的解析の方法を紹介します。

目次

この記事で取り上げている DDD のポリシーです。

本文

Tell, Don't Ask(命じろ、尋ねるな)

こちらは「DDD というよりはオブジェクト指向プログラミングとは?という内容だな」と思いながら読みました。
サンプルコードを使って説明します。

public class User {
    @Id
    public String userCode;
    public String firstName;
    public String lastName;
}

上記はユーザーを表すドメインモデル(エンティティ)です。
ここで fullName を作成する処理を考えてみましょう。
いわゆるトランザクションスクリプト的なコードでは次のようになります。

    User user = userRepository.findUser(userCode);
    String fullName = user.firstName + " " + user.lastName;

「Tell, Don't Ask」のポリシーに従うと次のようになります。

public class User {
    @Id
    public String userCode;
    public String firstName;
    public String lastName;

    // fullName を取得するメソッドを User に追加.
    public String fullName() {
        return firstName + " " + lastName;
    }
}
    User user = userRepository.findUser(userCode);
    String fullName = user.fullName();

後者のコードを書いてもらいたいので前者のコードを違反として検出したいです。
どのような静的解析を行えばよいでしょうか?

前者のコードでは User の属性(firstName と lastName)に外部からアクセスしています。
ドメインモデルの属性に外部からアクセスしているかどうかを検出できればチェックできそうです。
(「属性を private にすればいいじゃん」と思われるかもしれません。
それはその通りなのですが private にしてしまうとリフレクションによるマッピング(ORM など)が
できなくなってしまうので public なままにしておきたいのです)

そこで使うことにしたのが ArchUnit です。
ArchUnit については こちら を参考にしました。

ArchUnit を使って以下のテストコードを作成しました。

    @Test
    void doNotAccessFieldsOfDomainModel() {
        ArchCondition<JavaClass> doNotAccessFieldsOfDomainModel =
            new ArchCondition<JavaClass>("be as follows."
                    + " Do not access the fields of these classes directly.") {
                @Override
                public void check(JavaClass item, ConditionEvents events) {
                    for (JavaFieldAccess access : item.getFieldAccessesToSelf()) {
                        if (item != access.getOriginOwner() /* 当然ながら自分自身のフィールドへのアクセスは許容 */ ) {
                            String message = access.getDescription();
                            events.add(SimpleConditionEvent.violated(access, message));
                        }
                    }
                }
            };

        ArchRuleDefinition.classes().that()
            .resideInAPackage(ROOT_PACKAGE + ".domain..") /* ドメインモデルに対するフィールドアクセスが対象 */
            .should(doNotAccessFieldsOfDomainModel).check(CLASSES);
    }

このテストコードではドメインモデルに対するフィールドアクセスすべてが違反になりますが
「同じパッケージのクラスへのアクセスは許容する」といった例外を設けることも可能です。

依存関係逆転の原則 - ヘキサゴナルアーキテクチャ

こちらも ArchUnit を使います。
layeredArchitecture を使ってもできるのですが、ずばり onionArchitecture というルールがあるのでこちらを使います。
オニオンアーキテクチャとヘキサゴナルアーキテクチャはほぼ同じものです。

    @Test
    void onionArchitecture() {
        Architectures.onionArchitecture()
            .domainModels(ROOT_PACKAGE + ".domain..")
            //.domainServices(ROOT_PACKAGE + ".domain..")
            .applicationServices(ROOT_PACKAGE + ".application..")
            .adapter("presentation", ROOT_PACKAGE + ".controller..")
            .adapter("persistence", ROOT_PACKAGE + ".infrastructure..")
            .check(CLASSES);
    }

これにより以下の依存関係が担保されます。

  • presentation レイヤー(Controller)は他のレイヤーから依存されない
  • infrastructure レイヤーは他のレイヤーから依存されない
  • application services レイヤーは presentation, infrastructure レイヤーからのみ依存される
  • domain model レイヤーは presentation, infrastructure, application services レイヤーから依存される

domainServices のところをコメントアウトしていますが、これには理由があります。
オニオンアーキテクチャとヘキサゴナルアーキテクチャはほぼ同じものと書きましたが
オニオンアーキテクチャにだけは「ドメインサービス(上位)⇒ドメインモデル(下位)」という
依存関係が定義されています。

あくまでも取り入れたかったのはヘキサゴナルアーキテクチャであり、
IDDD 本にもドメインモデル(エンティティ)がドメインサービスに依存するサンプルコードも
載っていましたので domainServices は使わずに運用しています。

コマンド クエリ責務分離 (CQRS) パターン

こちらはまずサンプルコードから。

public class User {
    @Id
    public String userCode;
    public String firstName;
    public String lastName;

    @JsonIgnore
    public boolean active;
}

先ほども登場した User クラスです。
active という属性を追加していますが、ユーザーの有効/無効を表すものと考えてください。
active は登録・編集画面では更新できないので@JsonIgnoreを付けています。
(登録時、active にはデフォルト値を設定することになるでしょう)

登録・編集画面以外の箇所では必ず active を見てユーザーの有効/無効を判断しなければならないので
このようなドメインモデルになりました。

問題なさそうですが、さらによいモデリングはないのでしょうか?
登録・編集画面では知る必要のない active が見えてしまっているのに違和感があります。

おそらく RDB のユーザーテーブルは User クラスと同じような定義になっているのでしょう。
データモデル主体でモデリングするとこのようになりやすいです。

ドメインモデル主体でモデリングをやり直してみます。

public class UserRegistration {
    @Id
    public String userCode;
    public String firstName;
    public String lastName;
}
public class User {
    public UserRegistration userRegistration;

    public boolean active;
}

UserRegistration が登録・編集画面でのドメインモデル、User がそれ以外の箇所でのドメインモデルです。
登録・編集画面から active を隠蔽することができました。

前者のコードを違反として検出するには、、、@JsonIgnoreを禁止すればよさそうです。

これは Checkstle の config に以下を追加すればさくっと完了です。

        <module name="IllegalImport">
            <property name="illegalClasses"
             value="com.fasterxml.jackson.annotation.JsonIgnore"/>
        </module>

以上とりあえず対策したつもりですが問題があります。
今回の例はコマンドモデル < クエリモデルだったので@JsonIgnoreが出てくる可能性があったのですが
CQRS パターンが取り沙汰されるケースでは大抵の場合逆でクエリモデル< コマンドモデルです。
これはちょっと検出できません。。

ドメインモデルを適切に分離しないとあるクラスに責務が集中してしまうことになるので
1クラスあたりのコード行数上限を定めるというのが地味ながら有効な気がします。

まとめ

IDDD 本で取り上げられている DDD のポリシーと違反を検出するための静的解析の方法を紹介しました。
プロジェクトの方はまだドメイン部分の開発に入れていないのでまだですが ArchUnit を使うと
「境界づけられたコンテキスト」(パッケージ)間の依存関係のルールを作成することができるので
取り入れるつもりです。

ちなみに今回紹介した静的解析の中で一番検出率が高かった(一番引っ掛かった)のは
ヘキサゴナルアーキテクチャでした。
どのレイヤーに置くのがよいのか迷うクラス群があって頻繁にパッケージを変更していたのですが
静的解析で違反箇所が分かるのですぐに修正することができました。

goldmine
rakus
「IT技術で中小企業を強くします!」というミッションを掲げ、中小企業の業務効率化に貢献する複数のクラウドサービスを提供しているIT企業です。「楽楽精算」「メールディーラー」など、国内トップシェアを誇る複数のサービスを開発し、累計導入社数は5万社を超えています。次の時代の"楽"を創るための、まだ見ぬサービスや機能を生み出す取り組みは、今日も続いています。
https://www.rakus.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away