DDDに再入門しよう
同書の読書記録&感想の後編です。
- DDDに再入門しよう
- Chapter 10 データの整合性を保つ
- Chapter 11 アプリケーションを1から組み立てる
- Chapter 12 ドメインのルールを守る「集約」
- Chapter 13 複雑な条件を表現する「仕様」
- Chapter 14 アーキテクチャ
- Chapter 15 ドメイン駆動設計のとびらを開こう
- Appendix ソリューション構成
- まとめ:挫折せずDDDに入門できる本
Chapter 10 データの整合性を保つ
10.1 整合性とは
- 矛盾がなく一貫性のあること。
10.2 致命的な不具合を確認する
UserApplicationService#Register()
のこれまでのサンプルコードは登録前にユーザ重複チェックがあるが、複数ユーザから同時に処理すると登録できてしまう。
10.3 ユニークキー制約による防衛
- ユニークキー制約をつければ、コード中の重複チェックはいらなくなる…と考えるのは危険。コードから読み取れなくなる。またRDBのユニークキー制約という特定の技術基盤に依存してしまう。
- コードでの重複チェックとユニークキー制約、両方使うとよい。
10.4 トランザクションによる防衛
UserRepository
はメンバ変数でSqlConnection
型を持ち、コンストラクタ内でセット。Save()
などのメソッド引数にSqlTransaction
型を追加。SQL文実行の際にこのconnection
とtransaction
を使う。- 呼び出し元の外側、
UserApplicationService
もメンバ変数でSqlConnection
型を持ち、コンストラクタ内でセット。処理のメソッドで以下のようにする。
public void Register(UserRegisterComamnd command) { using(var transaciton = connection.BeginTransaction()) { // ユーザー生成処理、重複チェックの後に... UserRepository.save(user, transaction); // DBへの保存処理の引数にトランザクションを渡す transaction.Commit(); //ここでトランザクションをコミット } }
- 上記のトランザクション活用で整合性は担保できるが、RDBの技術基盤に依存してしまう。
- C#には、ここは整合性が必要な処理ですよ、と明示的に主張できる「トランザクションスコープ」という機能がある。RDBという特定の技術基盤に依存しない。
public void Register(UserRegisterComamnd command) { using(var transaciton = new TransactionScope()) { // UserRepositoryを使って // ユーザー生成処理、重複チェック... // DBへの保存処理をしてから... transaction.Complete(); } }
@Transactional public void Register(UserRegisterCommand command) { // ユーザー生成処理、重複チェック、 // UserRepositoryを使ってDBへの保存処理のコード // コード本体を書かなくてもメソッド正常終了でコミット、 // 途中の例外でロールバックを自動でやってくれる }
- ほかに、オブジェクトの変更を記録するオブジェクト「ユニットオブワーク」を使うパターンもある。
UnitOfWork
クラスがメソッドでpublic void RegisterNew(object value)
などを持つ- エンティティ用の抽象クラス
Entity
にMarkNew(), MarkClean(), MarkDirty(), MarkDelete()
などのメソッドを持ち、中はUnitOfWork.Current.RegisterNew()...
などを呼ぶだけ。 - この
Entity
クラスを継承したUser
クラスなどは、保存時にMarkNew()
などを呼ぶ。 - 呼び出し元の外側の
UserApplicationService
もメンバ変数でUnitOfWork
を持つ。Register()
などの最後にUnitOfWork
インスタンスのCommit()
を呼ぶ。 - この
UnitOfWork
クラスが中にメンバでUserRepository
を持つパターンもある。 - C#というか
.NET Core
,ADO.NET
がデフォルトで持っているデータアクセスの仕組み、Entity Framework
はこのユニットオブワークの実装である。 - 作者のnaruseさんはC#ならトランザクションスコープ、それ以外はAOPを使うとのこと。
- また、トランザクションが起こすロックはなるべく小さくすること。一度のトランザクションで保存するオブジェクトは少なく、小さいほうがよい。
UnitOfWork
パターンは明確には初めて知りました。なかなか変わったやり方に見えますがこういう解決方法もあるのですねえ。この章はあまりDDDとは関係なく一般的なトランザクションの話でした。
Chapter 11 アプリケーションを1から組み立てる
11.1 アプリケーションを組み立てるフロー
11.2 題材とする機能
- ユーザー機能を作ってきたので、複数ユーザーが入れるサークル機能を作る。サークルの作成、サークルへの参加が可能。
11.3 サークルの知識やルールをオブジェクトとして準備する
- サークルはライフサイクルがあるので、エンティティになる。
- 識別子として
CircleId
クラスを作成。Valueを持つだけ。 CircleName
もクラスとして作成。Valueを持つだけ。コンストラクタでサークル名制限のルールを記述。Equals()
メソッドを持つので、C#特有のIEquatabke<CircleName>
インタフェース実装で作成。Circle
クラスを作成。メンバにCircleId, CircleName, User型のowner, List<User>型のmembers
。- 永続化用に
ICircleRepository
を用意。メソッドはSave, Find
Circle
クラス生成用にファクトリとしてICircleFactory
を用意。メソッドはCircle Create(CircleName name, User owner)
- 重複確認はドメインサービスの仕事として
CircleService
クラスを作成。メンバにリポジトリを持ち、public bool Exists(Circle circle)
メソッドを用意。
11.4 ユースケースを組み立てる
- サークル作成コマンドは
CircleCreateCommand
として、中はuserId
とname
を持つだけ。 - 実際のサークル作成は
CircleApplicationService
クラスに。メンバでファクトリ、Circle
とUser
のリポジトリ、ドメインサービスの4つを持つ。 public void Create(CircleCreateCommand command)
メソッドの中でトランザクション生成、既存User
と既存Circle
の存在確認をして、リポジトリがSave
して作成し、トランザクション終了。サークル参加コマンドは
CircleJoinCommand
として、中はuserId
とcircleId
を持つだけ。- 作成済みの
CircleApplicationService
クラスにpublic void Join(CircleJoinCommand command)
メソッドを実装。トランザクション生成、User
とCircle
の存在確認、人数確認、circle.Members.add(menber)
でエンティティにそのユーザを追加して、circleRepository.Save(circle)
で保存、トランザクション終了。 - ここでサークルのメンバーは最大30人という仕様、ドメインのルールががある。オーナーが1人で残りが29人なので、コード内では
>= 29
で表現。ほかは>= 30
だったりわかりにくい。 - 例えばメンバー勧誘のユースケースが追加されたら、
public void Invite(CircleInviteCommand command)
メソッド追加。招待されるUser
と招待するUser
の存在確認、現在メンバーが>= 29
だったら中止、その後CircleInvitation
オブジェクトを作り、リポジトリがSave
。 - この数字の29や30がコード内のあちこちに散在しそうである。→次章の「集約」の考え方。
これは機能追加としてわかりやすい例ですね。DDDにちゃんと従うとクラス数がだいぶ増えますが、とても綺麗な実装になって理解しやすいです。
>= 29
の話は定数としてどこかに定義するのか、それとも判定のロジックをメソッド化して一箇所に定義するのかな、ドメインルールだからドメインサービス内だろうか?などと思いながら次へ...
Chapter 12 ドメインのルールを守る「集約」
12.1 集約とは
- データの変更単位となるオブジェクトをひとまとめにしたグループ。不変条件(厳密にはある処理の間、その真理値が真のままであること)を単位として切り出される。
- 集約に何が含まれるかの「境界」、外部からの操作を常に受け持つ集約の「ルート」であるオブジェクトを持つ。
(AR: Aggregate Root)
- 例えば
User
の集約はAggregate RootがUser
、内部にUserId
とUserName
。この3つを入れた丸い線が境界。ユーザ名を変更しようとしたら常にUser
クラスのChangeName()
メソッド経由となる。 - サークルの集約も同じでAggregate Rootが
Circle
、内部にCircleId
とCircleName
。この3つを入れた丸い線が境界。別の集約であるUser
と0...n
で線でつながる。たとえばサークルにユーザーを追加しようとしたら、circle.Members.Add(user)
のようにメンバ変数に直接アクセスはNG、circle.Join(user)
のようにメソッド経由が正しい。 - このように内部データにはアクセスせずそれを保持しているオブジェクトにメソッド経由で依頼する形にすると、より直感的、よりオブジェクト指向的になる。
→「デメテルの法則」(Law of Demeter)
、最小知識の法則。直接の友達とだけ話す。オブジェクトをまたいでまた別のオブジェクトを呼ばない。 なお実装的には内部で
<List>Users members;
で持っていても、集約の図ではこれは書かない。デメテルの法則に従うと、メンバー追加時の1サークル内上限は30人のルールを表すロジックは内部データに直接触れるのはNG。Circleクラスにチェックするふるまいを追加、外部からはこのメソッド経由で依頼するのが正しい。
public class Circle { private List<Users> members; public bool IsFull() { return members.Count >= 29; } }
- 上のやり方を徹底すればコード内のロジック散在も防げる。より柔軟なコード、メンテナンス性の向上が見込める。
- 内部データにgetterで触らないように徹底するには、ルールで定めた紳士協定とするやり方もある。
- 強制するには下の「通知オブジェクト」を使う方法もある。欠点はクラス数が増えること。
public class SomeRepository: IUserRepository { public void Save(User user) { // IUserNotificationを実装した通知Objで、メンバにUserIdとUserNameを持つ var notifyObj = new UserDataModelBuilder(); // Userクラス内のメソッドで、引数のObjにUserIdとUserNameを渡す。 // 中で完結するのでgetterで外から触らずデメテルの法則キープ! user.Notify(notifyObj); // 渡されたUserIdとUserNameを元に、Objが別のデータモデルクラスを生成。 var userDataModel = notifyObj.Build(); // このデータモデルクラスを使った別処理... } }
12.2 集約をどう区切るか
- リポジトリは変更の単位である集約ごとに用意する。
Cicle
がList<User> members;
を持っていてUserの変更もできてしまうと後からの修正が大きくなってしまう場合は、List<UserId> members;
のように識別子だけ持つ方法もある。インスタンスがないのでそもそもUser
の変更はできなくなる。メモリ節約にもつながる。
12.3 集約の大きさと操作の単位
- 大きいトランザクション=大きいロック範囲 なので、集約はなるべく小さい方がよい。複数の制約を同一トランザクションで操作するのもなるべく避ける。
- 避けられない場合は、クラウドでよく見られる「結果整合性」で行う方法がある。例えばユーザ名重複は大きい処理終了直後は担保されないが、1日1回のバッチ実行でしばらく経つと治って整合性が保たれる、など。
12.4 言葉との齟齬を消す
- コードと言葉の齟齬はなるべくないほうがよい。1サークル内上限30人のルールはCircleクラスにこう定義するとなお良い。
public class Circle { private List<Users> members; public bool IsFull() { // 別のメソッドも使う return CountMembers() >= 30; } public int CountMembers() { // 常に1名は管理者なのがこれでコードからも読み取れる...? return members.Count + 1; } }
- また集約の境界線を引いていく作業はドメインに寄りすぎてもシステムに寄りすぎてもダメで、バランスが大事。
本章のAggregate Rootとなる親のオブジェクトから下位のオブジェクトに線が繋がる集約の話は、マイクロサービスの文脈でも登場します。自分的には書籍『マイクロサービスパターン』によく出てきたのが思い出されます。
たとえばうまく境界線を定めてRestaurantがルートになるドメインができたら、マイクロサービスの単位もこのRestaurantがルートのレストランサービスになり、他のサービスからネットワークを介して変更の申請が来たらRestaurantが受け取って下位のオブジェクトも順々に更新していって...という具合ですね。
なるほどこう繋がるのかと納得でした。同書でも集約やトランザクションの単位はなるべく小さくと書いてあります。
「デメテルの法則」は普段はあまり意識していないのですが、オブジェクト指向のやり方に沿ってクラスに定義したメソッドに処理を任せるようにしていけば、だいたい自然とこうなるかなあという感じ。
通知オブジェクトを使う方法は面白いですね。ただクラスも増えて手間も増えるので、そこまでしてgetter使うの禁止にして意味あるの...?楽しい...?という気もします。
Chapter 13 複雑な条件を表現する「仕様」
13.1 仕様とは
- オブジェクトの評価処理はアプリケーションサービスに記述されてしまうことが多いが、ドメインの重要なルールである。評価処理をクラスに切り出したものを「仕様」と呼ぶ。
- 例えばサークルの上限判定、今までは
Circle#IsFull()
に実装。しかしプレミアムユーザの人数で上限が変わるという複雑な条件になったら? - ここで
CircleFullSpecification
という仕様クラスを新規作成。メンバにIUserRepository
を持ち、メソッドbool IsSatisfiedBy(Circle circle)
で判定条件を書くだけ。 すると
CircleApplicattionService
ではロジックの途中でこの仕様クラスを呼べば良い。メンバ変数では持たずメソッド内でnewして使う。別の話として
Cicle
内のユーザーをList
でなくList<User>
で持つCircleMembers
クラスを専用に作るような、「ファーストオブジェクト」パターンもある。上記の仕様クラスCircleFullSpecification
もメソッド引数がIsSatisfiedBy(CircleMembers members)
のように変わる。
13.2 仕様とリポジトリを組み合わせる
- 例えば最近一ヶ月以内に作られた、あるいはメンバ数10人以上のサークルを優先するお勧めサークル検索機能を作るとする。
- リポジトリの
CircleRepository
にメソッド追加だと、ドメイン内のルールがインフラの領域にはみ出てしまいよくない。 - 仕様クラス
CiecleRecommendSpecification
を新設。アプリケーションサービスからはまずリポジトリを呼んで全サークル検索、1件づつこの仕様クラスの判定メソッドに渡せば、リポジトリから判定条件は消える。 リポジトリのDB検索メソッドに
public List<Circle> Find(ISpecification<Circle> specification)
のように仕様クラスを渡すパターンもある。この仕様をリポジトリに渡すパターンのデメリットは、まずリポジトリがDB全件検索が必要な場合などの性能問題。DDD的に正解でも利用者に不便を強いるのはよくない。
- こうした場合はドメインオブジェクトから離れる場合もある。1回のSQL文発行の中に条件を全部入れて解決、ORMを利用するなど。例ではリポジトリではなく
CircleQueryService
という別クラスで実現。 - DBへの書き込み処理(コマンド)はドメインオブジェクトを積極利用、読み取り(クエリ)ではある程度ゆるくするやり方はコマンドクエリ分離の原則
(Command-Query Separation:CQS)
、コマンドクエリ責務分離の原則(Command-Query Responsibility Segregation:CQRS)
という考え方もある。 - 類似してC#の
Entity Framework
では検索結果のコレクションに条件が適用されたときに初めて実際のDB検索が行われる、「遅延実行」が実装されていたりする。
CQRSの考え方は書籍『マイクロサービスパターン』でも紹介されていました。真面目に作ったら各マイクロサービスに散らばってしまうようなデータを、検索専用ビューを用意して各サービスで検索時に限ってはこちらを使う。この検索専用ビューの中のデータ更新は別タイミングで行われる...というものですね。
やはりドメインの設計原則の理想に従うだけでは現実的には上手くいかない場合もあるのだなあと思いました。
Chapter 14 アーキテクチャ
14.1 アーキテクチャの役目
- アーキテクチャは知識を記述する箇所を示す方針。ドメインのルールが外に漏れ出すことを禁止し、一箇所にまとめることを促してくれる。
- 何がどこに記述されるべきかという疑問に対する回答を示してくれるので、開発者は「ドメインを捉え、上手く表現する」ことに集中できる。ソフトウェア特有の事情からドメインオブジェクトを防衛できる。
- DDDにとってアーキテクチャは主役ではないので、どのようなものでもよい。
14.2 アーキテクチャの解説
よく語られるものが3つ。
レイヤードアーキテクチャ
- エヴァンス本でも言及されており、そこでのレイヤーは4つ。
- プレゼンテーション層(ユーザーインターフェース層):UIとアプリケーションを結びつける層。WebやCLI。
- アプリケーション層:ドメイン層のものをまとめる層。ドメイン層の呼び出し元のクライアントになる。DDDではアプリケーションサービス。ドメイン層もインフラ層も呼ぶことがある
- ドメイン層:ドメインオブジェクトはここに隔離し、他の層に流出させない。
- インフラストラクチャ層:他の層を支える技術的基盤へのアクセス。永続化やメッセージ送信。
- 依存は常に上から下、プレゼンテーション→アプリケーション→ドメイン→インフラ
- エヴァンス本のシーケンス図ではユニットオブワークのパターンが使われており、昨今では珍しい。
実装例が以下のクラス群。
UserController:
プレゼンテーション層。メンバ変数にUserApplicationService
を持つ。GETやPOSTでリクエストが飛んできたらCommnd
クラスを作って引数にしてUserApplicationService
の各メソッドを呼ぶ。
UserApplicationService:
アプリケーション層。メンバ変数にUserFactory, UserRepository, UserService
を持つ。Get
, Register
, Update
などのメソッド内でドメイン層のドメインオブジェクトを作ったり、ドメインサービスのロジックを呼んだり、リポジトリから検索したり保存したり。メソッドの戻り値がUserRegisterResult
などResult
クラスを使っている。
User:
ドメイン層のドメインオブジェクト。
UserService:
ドメイン層のドメインサービス。メンバ変数にUserRepository
がいてロジック処理など。
EFUserRepository:
インフラ層。IUserRepositoryを実装、技術基盤と結びついて永続化処理。
ヘキサゴナルアーキテクチャ
- 六角形の中心に
Application
。中心と周囲とつなぐAdapter
があり、その外側にWebやDBがいる。 - ゲーム機の例えが分かりやすい。中心にあるゲーム機本体からしたら繋がりさえすればコントローラーが複数種類あってもよいし、モニターも複数あってもよいし、データの保存先はハードディスクでもクラウドでも良い。
- ポートアンドアダプターパターンとも呼ばれる。アプリケーションへの入力がプライマリポート/アダプター、アプリケーションが外部へ出力するのがセカンダリポート/アダプター。ポートが抽象で実装したオブジェクトがアダプタ。
- これまで例に出てきた
UserApplicationService
クラスがこれを実現している。このクラスの呼び出しクライアントがプライマリアダプタ。Update()
などのメソッドがプライマリポート。このクラスが呼び出すIUserRepository
がセカンダリポートになる。
クリーンアーキテクチャ
- 4つの同心円からなるアーキテクチャ。真ん中にある
Entities
はDDDのエンティティとちょっと違い、ビジネスルールをカプセル化したオブジェクト。ドメインオブジェクトには近い。 - UIやデータ永続化の仕組みは端に追いやって、依存は常に円の中心へ。ヘキサゴナルアーキテクチャと目指すところは同じ。
- インターフェースを活用して依存の方向を制御する。
- どのアーキテクチャでも人間が一度に多くのことを考えすぎないよう、方針を定めてくれる。考える範囲が狭まることでより深いモデルの考察ができる。ドメインの隔離を促すことができればDDD的にはどのようなアーキテクチャでも良い。
レイヤードアーキテクチャのアプリケーション層のサンプルコードのメソッド戻り値のResultクラスというのが前は出てこなかったような…? あとヘキサゴナルアーキテクチャのポートとアダプターの差がよくわかりませんでした。
クリーンアーキテクチャは有名なので本で読んだ方も多いのではないでしょうか。当ブログにも感想記事があるのでこちらもどうぞ。
DDDと用語や考え方がまた若干違います。アプリケーションのロジック部分のコードを中心にして、周囲の技術基盤に影響されないように、変更に強いコードにしていこうという根っこの考えは同じですね。
Chapter 15 ドメイン駆動設計のとびらを開こう
最後はまとめの章。
15.1 軽量DDDに陥らないために
- パターンだけを取り入れる手法を軽量DDDと呼ぶ。DDDが目指すのはパターンに終始することではなくドメインの本質に向き合うことなので、これはよくない。
- そのドメインの精通者、実践者(ドメインエキスパート)とよく会話して、その人達の言葉で考えていく。
- しかしドメインエキスパートがドメインモデルを知っているわけではないのでモデリングが必要。ドメインとコードはモデルを通じて繋がる。
15.3 ユビキタス言語
- 混乱を避け翻訳の手間を避けるため、プロジェクトの共通言語をユビキタス言語と言う。ドメインエキスパート側の言葉ベースだが不正確だったら改良していく。
- ユーザーの「名前を変更する」がそのドメインで適切な表現だったら、コード側のクラスメソッド名も
UpdateName
でなくChangeName
でよい。
15.4 境界付けられたコンテキスト
- 例えばこれまで例に上がってきた
User
やCircle
クラスで、急遽システムにログイン用のパスワードが出てきたらどうするか。元のUser
の活動にログインは関係ない。 - こうした場合はC#ならこれまでの
User
クラスはCore.Model.Users
名前空間へ。認証用の別のUsers
クラスを作ってAuthenticate.Model.Users
のような別の名前空間に配置するとよい。別のほうのUser
クラスはメンバ変数にUserId
とPassword
を持つ。こうした区別を「境界づけられたコンテキスト」と呼ぶ。
15.5 コンテキストマップ
- コンテキスト同士の関係を定義し、ドメイン全体を俯瞰できるようにした図を作ると良い。
Authenticate
とCore
の大きな丸があり、その中にUser
やCircle
クラスがあって線で関連を繋いだりするもの。- 境界づけられたコンテキストがそのまま開発チームの境界になったら、互いに協力していく。
Appendix ソリューション構成
A.1 ソフトウェア開発の最初の一歩
Visual StudioでC#で開発する場合の限定ですが、ソリューション構成の例が載っていて何気にとてもお役立ちです。他の言語でもパッケージ、フォルダ構成の参考になるでしょう。
ドメインレイヤーの構成例が以下。
Domain/ // 実際には境界づけられたコンテキストの名前になる Models/ Circles/ // ここに値オブジェクト、エンティティ、 // リポジトリやファクトリのインターフェース、仕様クラスも Users/ // 同様 Services/ // UserServiceのようなドメインサービス Shared/ // ISpecification のような他のプロジェクトでも共用できるもの
ファクトリやリポジトリはクラスの種類によってフォルダ分割せず、ドメインオブジェクトと一緒のフォルダに置くのがポイント。
アプリケーションレイヤーが以下。
Application/ Circles/ // アプリケーションサービス、Commandクラス、Resultクラス、DTOなCircleDataなど Users/ // 同様
こちらはアプリケーションサービスごとにフォルダ分け。クラスが大きくなってきたらクラスの種類によって細分化もあり。クリーンアーキテクチャ寄りにするとCircles/ の下がさらにCreate/, Get/ などに細分化。
インフラストラクチャのレイヤーが以下。
EF/ // 技術基盤にEntity Frameworkを使ったクラス群 Circles/ // リポジトリ、対象ドメインオブジェクトのFactory Users/ // 同様 InMemory/ // テスト用にメモリ上に保存するだけのクラス群 Circles/ // 同様 Users/ // 同様
こちらは技術基盤ごとにフォルダ分け、一緒にしてもよいとのこと。
A.2 ソリューション構成
C#では.sln
ファイルを開くとVisual Studio上に展開されるひとつのソリューション(複数プロジェクトをまとめたようなもの)があり、その中に1~個のプロジェクトがあります。この構成について。
ドメイン/アプリケーション/インフラストラクチャーがすべてひとつのプロジェクト:
非推奨。
ドメイン/アプリケーション/インフラストラクチャーがすべて別のプロジェクト:
なおインフラはEFInfrastructure
, InMemoryInfrastructure
など技術基盤ごとにそれぞれ別プロジェクト。
ドメインオブジェジェクトのメソッドはpublic
にする必要があり、呼ぶべきでない場所のクラスから呼べてしまう可能性が増える。
ドメイン/アプリケーションが同じプロジェクト、インフラストラクチャーだけ別:
こちらもインフラはEFInfrastructure
, InMemoryInfrastructure
など技術基盤ごとにそれぞれ別プロジェクト。
ドメインオブジェジェクトのメソッドはinternal
にすれば、他のプロジェクトからは呼べなくなって安全。
このへんはプログラム言語の特性によって差異あり。なぜその構成にしたのか理由付けをはっきりしておく。
考え抜かれたソリューションには美しさが宿る...なんてあって、これは全体の構成を決める立場だと分かるなあというところ。
まとめ:挫折せずDDDに入門できる本
ネットの書評も良い本書ですが、確かにボトムアップで丁寧に解説してくれていてありがたいです。長い本ですが章立て、中身も細分化されて個々の話は短いので割とすんなり読めます。
読者に理解させる意図を徹底しているのか、例も簡単で分かりやすいです。サンプルコードのC#も例が簡単なので最初はUserクラスの話が延々と続くようにも見えますが、徐々に本格的になっていきます。
リポジトリでDBに接続する部分のコードなど、技術基盤に依存した一部を除くと特に難易度は高くなく、普段C#をお使いでない方でもオブジェクト指向言語がある程度分かる方なら雰囲気で理解できると思います。
自分は完全なDDDを仕事のプロジェクトで実践している訳ではないし、別に導入しなくても変更に耐えうる設計で問題なくやれてるのよね〜というスタンスですが、それでも学ぶところのある本でした。DDDを徹底していなくても、ふだん自分が設計で気にかけているところなんかが予想した以上に共通しているなあというところ。これで...エヴァンス本も読める...かも!(※死亡フラグ)
特にプログラミングがある程度できるようになってクラス設計で悩んだり、システム全体の設計やアーキテクチャが気になってきた頃、中級者ぐらいの方にはDDDを実践しているかに限らず役に立つと思います。
第14章メインでサンプルコードもあります。
作者のnrsさんのブログに後書き記事もあります。
関連書籍
『現場で役立つシステム設計の原則』もDDDの考え方に寄っていることで有名な本。現場力が高まるのでこの本もお勧めです。当ブログにも感想記事があります。
その続き的な位置づけの『実践ドメイン駆動設計』。
『「実践ドメイン駆動設計」から学ぶDDDの実装入門』という本もあります。
再掲ですが『Clean Architecture』もDDDと並んでよく話題に上がります。この本もお勧めです。当ブログにもまとめ&感想記事があります。