🙌

[読書メモ]ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本をざっくり読んでみた

に公開

最初に

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本を読んでいる際に、個人的につけていたメモです。

いわゆるまとめ記事ではなく、内容も飛び飛びで自分の感想も多々混ざっていますが、ご了承ください。

今まで読んだドメイン駆動設計に関する本の中で最もサンプルコードが多く、理解しやすい書籍でした。
また、ドメイン駆動設計に関する各パターンを導入するうえで感じられる効果やなぜ良いのかを細かく説明することで、導入のハードルを下げさせてくれる気づかいを感じたのが非常に良かったです。

https://amzn.asia/d/br4V00Z

Chapter1:ドメイン駆動設計とは何か

ソフトウェア開発におけるドメインとはすなわち「プログラムを適用する対象となる領域」を指す。
ドメインとは人の営みそのものを表しており、すべてに対してプログラムを適用することは難しく、本来不必要な情報がドメインには含まれていることもある。
プログラムを適用する対象となる領域において、システムに落とし込んでいくうえで抑えておくべき事象や概念を可視化したものがドメインモデルであり、可視化する営みがモデリングとなる。
モデリングした成果物を用いて実際にコードに落としたものがドメインオブジェクトとなり、システム全体を構成する一部分となる。

ドメインは現実世界を表しているため、当然素早く変化し、変化はドメインモデルへ伝搬していき、最終的にドメインオブジェクトとドメインとの間でギャップが生まれる。
そこで、常にモデリングを繰り返すことで、ドメインモデルとドメインオブジェクトに大きな差分が出ていないかを確認して、ブラッシュアップを繰り返していくことがドメイン駆動設計の一連のサイクルになるのかもしれない。

Chapter2:システム固有の値を表現する「値オブジェクト」

値オブジェクトはシステム固有の値を表したオブジェクトのことを指す。単なる文字列変数を氏名として扱わず、姓と名それぞれを所有した別のオブジェクトとして扱うのも値オブジェクトの例の一つとなる。
値オブジェクトは要はプリミティブな型の値をラッパーしたものであるが、同様に値として以下の性質を保有する

  • 不変である
  • 交換が可能である
  • 等価性によって比較される

不変である

ここでいう不変というのは値そのものが変化することはないという意味になる。
普段何気なく行っている変数の代入では、もともと変数に入っていた値が新しい値に上書きされている。代わりに入れるからこそ代入と呼ばれる。
これは、値そのもの(代入される前の値)そのものを別のものに変化させているのではなくて、ある変数に入れる値を別のものに置換しているといえる。

a = "hello"
a = "Bye"

というコードがあったとして、hello自体がByeという文字列になっているのではなくて、aの中に入る文字列の値が"hello"から"Bye"に置き換わっている、と言える。
的なことが書いてあり、無茶苦茶理解できるが、難しい。

"hello" = "Bye"

はおかしいというのが直感的に分かるのは値が不変ということを無意識で理解しているからかもしれないと納得した。

交換が可能である

不変であるの説明に重複する。値そのものを変えられないが、別の値と交換することはできる。すなわち代入のみが変更を表現する唯一の手段となりえる、というのが値が持つ性質の1つとなる。

等価性によって比較される

「等価性」とはなんぞやというと以下のような意味らしい(ChatGptに聞いてみた)

「値オブジェクト(Value Object)」の特性として言われる「等価性によって比較される」というのは、オブジェクトの“同一性”ではなく、その中に保持している“値”が同じかどうかで比較する、という意味です。具体的には次のようなことを指します。

分かるようなわからないような。。例えば2つの100円硬貨があったとして、物質的な意味ではそれぞれ違うタイミングで作成されたものであり、それぞれユニークな硬貨にみえるが、同じ100円という価値を持っているという意味で「同じ」であり、すなわち等価性によって比較すると等しい、ということになる。

値オブジェクトの話で言うと、異なるモジュールの内部でそれぞれ作成されたインスタンスでも、属性が一致しているのであれば等しいとみなすわけであり、つまり「等価性によって比較」されているということになる。

値オブジェクトにする基準

ドメインモデルとして挙げられた概念以外の値を値オブジェクトにするか否かの判断基準として以下がある。

  • そこにルールが存在しているか
  • それ単体で扱いたいか

例えば氏名を表す値オブジェクトが姓と名それぞれを値オブジェクトとして保有する場合、姓名それぞれに固有のルールが存在しているか、それぞれを別の概念として扱いたいか、で判断することがポイントとなる。

重要なのは値オブジェクトを避けることではなく、値オブジェクトにすべきか否かを判断し、その判断に素早く従って実行することとなる。また、そうした営みの中で判明した事実に基づきドメインモデルを洗練化し、ドメインをドメインモデルでより精緻に表現できるようにフィードバックするサイクルを回すことが肝要となる。

Chapter3:ライフサイクルのあるオブジェクト「エンティティ」

エンティティも値オブジェクト同様、ドメインモデルに基づき実装されたドメインオブジェクトである。値オブジェクトとドメインオブジェクトを分ける基準として、同一性(identity)に基づいて識別されるか否かがある。

同一性によって識別される

「同一性」とはなんぞやというと以下のような意味らしい(ChatGptに聞いてみた)

同一性(Identity)
あるエンティティが「同じ存在」であるかどうかを示す不変の識別子。例えるなら、あなたの「マイナンバー」や「社員番号」のようなものです。

値オブジェクトがそれらの持つ属性の値が一致しているかで比較をするのに対して、エンティティにおいては不変の識別子を用いて比較を行う。なぜかというと、そもそもエンティティはエンティティ自身が持つ属性が変化しうることを前提としていることを加味すると理解しやすい。

例としてある住民の氏名や住所はさまざまなイベントで変化するものの、マイナンバーは不変のためマイナンバーが一致している、すなわち同じ人間という比較が行える。

以上の話も含めると、エンティティが持つ特徴はいかとなり、値オブジェクトと対の存在であると理解できる

  • 可変である
  • 同じ属性であっても区別される
  • 同一性によって区別される

値オブジェクトとエンティティの使い分け

値オブジェクトとエンティティを使い分けるための基準として「ライフサイクルが存在し、連続性を持っているか」がある。

例えばユーザーであれば、作成・変更・削除といった変遷を得る特性を持った概念であることからエンティティが望ましいと判断できる。しかし、可変なオブジェクトはシステムで管理する際に慎重さが求められるデメリットもあるため、ライフサイクルを表現することがシステム上無意味である場合は、値オブジェクトとして表現するのが望ましい。

とはいえ、扱う際の文脈によっては値オブジェクトとエンティティのどちらにもなりうるオブジェクトというは存在する。

Chapter4:不自然さを解決する「ドメインサービス」

例えば新規ユーザー作成時に重複した名前のユーザーが存在しないかをチェックする機能など、値オブジェクトやエンティティに実装することで違和感を感じる処理というのが発生する可能性がある。

本来ユーザーを意味するオブジェクトに重複チェックの処理を実装し、そのためのインスタンスを生成してチェックを行うなどといったいびつな共通化はコードにゆがみをもたらしてしまうので健全ではない。

そういった実装を別途専門して行うクラスとして実装されるのがドメインサービスである。ドメインサービスは、自身の振る舞いを変更するようなインスタンス特有の状態を持たない。

ドメインサービスを導入することで先述した例のような違和感を解消できるが、あらゆる処理をドメインサービスに実装できてしまうため、ドメインモデルが貧弱になるドメインモデル貧血症を引き起こしてしまう。そのため、ドメインサービスは極力避けるスタンスで、どうしても使いたい場合に利用する、という方針が実装を薦めるのが望ましいらしい。

Chapter5:データにまつわる処理を分離する「リポジトリ」

リポジトリとはデータを永続化させるためのデータストアへの口となるのがリポジトリである。
値オブジェクトやエンティティが直接SQLを実行する実装だと、ドメインオブジェクトが技術的な制約で汚れていく恐れがある。また、単純に可読性も下がってしまう。
そこで、ドメインサービスはリポジトリのインターフェースに依存するようにして実態はリポジトリの実装に隠ぺいしてしまうのが望ましい。

そうすることで、仮にデータストアとして用いている技術が変更となったとしてもリポジトリの実装を取り換えるだけでよくなる。
これは、依存性逆転の原則で言われる非常に有名なメリットだ。外部から注入できることで、テストの際はインメモリのリポジトリを用いるようにもできるのが非常に柔軟性が高いと言える。

リポジトリはあくまで情報の永続化の口を提供することがメインの目的であることから、重複ユーザーのチェックといったドメイン的な知識を要する実装をそのまま実装してしまうとドメインサービスからドメイン知識が失われてしまうので、単純に実装するのは望ましくないといえる。リポジトリのInterfaceとしては何らかの識別子によってDBからselectしてくるFindというメソッドを用意し、それをドメインサービスを用いるという構造にするのがよい、という考えがとても腹落ちした。

この書籍は値オブジェクトとエンティティの使い分けといった異なる概念に対して線引きするための基準を作ってくれる記述が多いのが非常に良い。

Chapter6:ユースケースを実現する「アプリケーションサービス」

アプリケーションサービスはドメインオブジェクトやリポジトリを使いこなすことで、ユースケースを達成するためのサービスを提供する。
アプリケーションサービスを実装する際に、ドメインオブジェクトをそのまま公開するかどうかというのが重要な決断となる。

ドメインオブジェクトをそのままむき出しにした場合、アプリケーションサービスからクライアントへドメインオブジェクトをそのまま返すだけでよくなるため、記述がシンプルになる。その一方でアプリケーションサービスのクライアントが無条件でドメインオブジェクトの各メソッドにアクセスできてしまうため、本来アプリケーションサービス側で提供すべき実装がアプリケーションサービスのクライアント側に散らばって実装される恐れがある。

そこでの解決策として、ドメインオブジェクトをDTOに変換したうえでクライアントに返すというのがあげられていた。
クライアントが本来必要とする値のみをDTOとして組み立ててから返すことで先述したようなコードの重複をはじめとしたリスクを低減できる。

ドメインオブジェクトをそのまま返す方がシンプルなため、いつでも最適解とは言えないかもしれないが、DTOに詰め替える手間に比べて堅牢さのメリットの方が大きいので、積極的に採用してよい気もした。
ドメインオブジェクトからDTOに詰め替える部分を別途メソッドとして切り出して、DTOに変更が入った場合の修正を一か所に集約させる、というテクニックが有用だと感じた。

ドメインサービスとアプリケーションサービス

ドメインサービスもアプリケーションサービスもクライアントのために何かを行うものという点では一致しており、向き先がドメインなのかアプリケーションなのかという違いでしかない。
どちらも振る舞いを変化させるために状態を保持しない。これは、状態を一切持たないということを意味しているのではなくて、「振る舞いを変化させるため」というのがミソになる。
例えば、アプリケーションサービスがリポジトリを保有してエンティティの永続化を依頼する場合は全く問題ない。

しかし、以下のように、アプリケーションサービス自体が保有する値がスイッチの役割となって振る舞いを変化させている場合は、「振る舞いを変化させるために」状態を保持しているため望ましくない。

なぜならば、サービスが今どのような状態にあるのかを逐次気にしながらサービスの実装をする必要が出てくるためである。

そもそも、アプリケーションサービスが何らかの状態によって切り替わっているということはいびつな共通化や本来異なる目的のものに既存の機能を無理やり流用しているよう可能性が高いので、それぞれ別個のサービスとして分けたりするのも考える必要がありそうだなと思った。

class UserApplicationService:
    def __init__(self, is_admin_mode: bool):
        self.is_admin_mode = is_admin_mode

    def delete_user(self, user_id: int):
        if self.is_admin_mode:
            print(f"[Adminモード] ユーザー {user_id} を強制削除します。")
        else:
            print(f"[通常モード] ユーザー {user_id} を削除リクエストに送信します。")

Chapter7:柔軟性をもたらす依存関係のコントロール

全体的に依存性逆転の原則に関する解説と具体的に利用されるDIといったデザインパターンの解説が行われている章だった。
プログラムにおける依存というのは通常抽象から具体に矢印が向くこととなり、具体に対して依存するような実装となる。

人間に近い抽象的な動作やユースケースを実現するためのアプリケーションサービスが具体的な技術の詳細にあたるリポジトリに依存する形になるのはそのためである。
アプリケーションサービスのような比較的上位の存在が下位の存在、すなわち具体に依存するのではなくて、上位の存在が要求する定義、すなわち抽象に依存をするように変えることが依存性の逆転といえる。通常であれば抽象⇒具体となっていたところが抽象⇒抽象となり、定義されたインターフェースに従って具体が実装されることで具体⇒抽象への依存関係が向けられることとなる。

Chapter8:ソフトウェアシステムを組み立てる

Chapter7までで実装したアプリケーションサービスなどをフレームワークから呼び出す形にして実際にソフトウェアとして組み立てる過程がステップとごとに説明されていて非常に分かりやすかった。
内容自体はそこまで複雑に感じず、理解しやすかったのはサンプルとしてソフトウェアの内容が単純化されていた、ということもそうなのだろうけど、そもそもそういった複雑性の高い部分が隠蔽されている良いアプリケーションサービスとなっているからこそ、Chapter8の内容も理解しやすいのだろうなと思った。

良かったところの引用(P.203)
テストがしやすいコードは良いコード、ですね。

実際にユーザーインターフェースを交換するような事態は稀ですが、ユーザーインターフェースを交換可能であるということはアプリケーションが単独で実行できるということで、つまりユニットテストを実施できるということに他なりません。
ユニットテストがそのままソフトウェアの品質向上になるわけでは必ずしもありませんが、ユニットテストができるような形に仕立てることは品質向上の第一歩です。

Chapter9:複雑な生成処理を行う「ファクトリ」

ドメインオブジェクトの生成方法が複雑になってくると、コンストラクタから直接生成するよりもファクトリを別途用意してファクトリを通じてインスタンスを生成するようにするとクライアント側のコードの見通しが良くなる。またファクトリ自体を外部から注入できる形にすることでテストがしやすくなったりといったメリットもある。

ファクトリを作る際の動機づけとして「コンストラクタ内でほかのオブジェクトを生成するか」というものがある。
これは、エンティティの内部で引数で与えられた値に基づいて値オブジェクトを作成してプロパティとして属性に持つ場合も同様と言えそう。
これらをすべてコンストラクタに引数として設定し、クライアントから必要な情報を渡してもらうのも手だが、ファクトリを通じて生成することで改善できないかを検討する価値はありそう。

Chapter10:データの整合性を保つ

システムにはデータの整合性が求められる処理が存在する。整合性を担保するためには、トランザクションなどといった詳細技術に依存した実装が必要となり、アプリケーションサービスの実装が詳細技術の都合で引っ張られる形になりがち。どうやって整合性を担保するかも重要ではあるが、ビジネスロジックとしては、どの処理部分が整合性を保つべきかを明示的に主張することが求められる。

C#でいえればトランザクションスコープを用いるのもその一種といえる。

using System.Transactions;

public class OrderService
{
    private readonly IOrderRepository _orderRepo;
    private readonly IInventoryRepository _inventoryRepo;

    public OrderService(IOrderRepository orderRepo, IInventoryRepository inventoryRepo)
    {
        _orderRepo      = orderRepo;
        _inventoryRepo  = inventoryRepo;
    }

    public void PlaceOrder(int productId, int quantity)
    {
        using (var tx = new TransactionScope())
        {
            _orderRepo.Save(new Order(productId, quantity));
            _inventoryRepo.Allocate(productId, quantity);
            tx.Complete();
        }
    }
}

ユニットオブワーク

ユニットオブワークとはあるオブジェクトの変更を記録するオブジェクトである。
変更・削除などの際にユニットオブワークに通知の身を行い、データストアに反映を行わず、commitが通知されたタイミングでまとめて変更内容をデータストアに保存する、というパターンになる。勉強にはなったが、トランザクションスコープ使えるならそっちの方がハードルは低いなと感じた。

Chapter11:アプリケーションを1から組み立てる

Chapterタイトルの通り、これまでのChapterで出てきた技術やパターンを用いて改めて新しい機能を実装する流れが述べられていた。内容については復習がメインだったので割愛するが、なんだったっけこれ?となるところを復習できたので良かった。

また、実装していく中でドメイン知識がアプリケーションサービスに漏れ出していることでの設計上の不穏なにおいを指摘して、次の「集約」の話につなげているところが非常にスマートだと感じた。

Chapter12:ドメインのルールを守る「集約」

オブジェクト指向プログラミングでは複数のオブジェクトが組み合わさってひとつのオブジェクトが構築される。また、構築されたオブジェクトのまとまり(グループ)において、ある処理の間常に真のまま変化しない術後のことを「不変条件」という。

不変条件を守るために、まとまった単位で切り出されたものが集約となり、集約に対する捜査は集約ルートを必ず経由して行われることで集約内の不変条件が常に維持された状態を保てるようになる。

何らかのオブジェクトの内部の値にアクセスしてそのうえでビジネスロジック的な判定をしている場合、不変条件が壊れてしまう可能性がある。また、本来ある集約の内部で実装すべきビジネスロジックの判断が分散することで、コードの重複が発生し、変更容易性が下がるリスクもある。集約は変更の単位で区切るのは望ましいらしい。

ゲッターが非推奨となるのは、上記のリスクを招くからである。

そもそもゲッターによって得られた値を用いて行っている処理がドメインオブジェクト側で巻き取れる可能性があるかもと考えるのが重要だなと思った。

Chapter13:複雑な条件を表現する「仕様」

あるオブジェクトが特定の基準を満たしているかどうかを確認するためのオブジェクトのことを「仕様」と呼ぶ。
例えば、サークル活動を管理するシステムにおいて、サークルが満員であるかを確認する場合などがある。

これらの判断はリポジトリを経由してDBから取得した値に基づいて判断する、といった流れになりやすく、リポジトリを保有するアプリケーションサービス上で実装するのが一番素直な方法となる。
しかし、何らかの条件を満たしているかどうかというのはドメインのルールに基づく重要な判断であり、アプリケーションサービス上に漏れさせるべきではないといえる。
仕様とは、それらの判断に特化して処理を行うためのオブジェクトとして実装されるものであり、アプリケーションサービスは仕様を通じて、ドメイン上の判断を行うようになる。

別の話として、リポジトリに検索を行うメソッドを実装する場合そのメソッドの中にドメインの重要なルールを含むものが時に存在する。
これもまたリポジトリ内部にドメインルールが漏れ出してしまうパターンの1つとなるため、仕様として別のオブジェクトにルールを実装している部分を切り出して、仕様からリポジトリを通じて任意のデータを検索してくる、といった形にするのがきれいな形の1つと言えるかもしれない。

パフォーマンス面で問題が生じる場合があるため、遅延実行やクエリ押し下げなどの手法を併せて検討するとよいらしい。

Chapter14:アーキテクチャ

ヘキサゴナルアーキテクチャ、レイヤードアーキテクチャ、クリーンアーキテクチャ、それぞれ細かい違いはあれど、ドメインにすべての依存を集中させて、ほかのレイヤーににじませないという点に関して全く同じであり、ドメイン駆動開発を行っていくうえでもどのアーキテクチャかは重要ではない。

ビジネス的な判断を多く行う「利巧なUI」ではなくて、ユーザーからの入力を返還してアプリケーションサービス伝達する「愚かなUI」の方が変更容易性が高いUIと言える。
UIは特定のフレームワークだったり、ユーザーの使いやすさなどで柔軟に変化しうる箇所だからこそ、それらのコントロールに集中する形にして、ビジネス的に重要なロジックはドメインに追いやることできれいに隔離されたプログラムがつくられる。

ドメインをほかのレイヤーから明確に「隔離」させることが最も肝要なのかもしれない。

Chapter15:ドメイン駆動設計のとびらを開こう

ドメインエキスパートと協力して、ドメインモデリングとシステム開発を反復させていきながら洗練させていくことが大事だよ、業務部門と開発者が同じ目線で同じ言葉で語ることにコストを払ってユビキタス言語に基づいてドメインを正しく表現するドメインオブジェクトやシステムを作り上げよう、という感じのお話だった。

特にユーザー名を変えることは直感的には「ユーザー名の変更」だが、データストアの値を変えるということから「ユーザー名の更新」ととらえてUpdateNameとかにしちゃいそう~!というかやってるわ・・・となった。

別の書籍で変数名に技術駆動の命名をするなというのがあったがそれも近い話かもしれない。変数名やメソッド名、クラスの名前、小さいところではあるがそういったところからボトムアップに積み上げていくことがドメイン駆動開発の第一歩なのかもしれないなと思った。

Discussion