はじめに
9月末に、Biglobeで開かれた「レガシーなコードにドメイン駆動設計で立ち向かった5年間の軌跡」に行き、
興味を持ったDDD。
とはいえ実践の機会がずっとないまま過ごしていたのですが、
ふとしてタイミングで仕事で活かせる(かも)という機会がやってきました。
ただ、エリック・エヴァンスのあの本はちょっと敷居が高い…というか重い!
本屋で見てみたけれど、買う勇気が…。
そんなタイミングで「第3回ボトムアップドメイン駆動設計」の補欠繰り上がりの連絡が来たので、
2/28、意気揚々と参加してきました。
全体を通して、DDDのドメインを切り出す作業(設計)ではなく、
DDDでどう実装するか? を明らかにしてくれるイベントでした。
DDDを扱う上で出てくる理解しづらい単語を1つずつ、解説してくれました。
GMOInternetのデベロッパーエバンジェリスト、成瀬さん(@nrslib)が、
スピーディーながらもわかりやすく説明してくださった資料はこちら。
コンテンツ
なぜドメイン駆動設計は怖いのか?
知らないことが多いから怖く感じる。
用語と実装がわかれば、怖さが半分になるのでは?
鈍器のようなエヴァンス本を読むための下地を作ることがこのイベントの目的だそうです。
値オブジェクトとは
- 状態を不変に保つ
- 値同士の比較ができる
- 完全に交換可能である
の3つの特徴を持つもの。
1. 状態を不変に保つ
状態=フィールド=クラスにおけるメンバ変数。
状態を変更したいときは必ず代入する。
可変な状態は扱いが難しいから、あえて不変にする。
2. 値同士の比較ができる
1==1を1.value==1.valueとは書かない。
オブジェクトを比較するときも、オブジェクト同士をまるごと比較するEqualsメソッドを作る。
そうすると、変更箇所がequalsメソッドに集約できる!(要素が増えたときの変更箇所が1箇所で済む)
(※比較する必要ができたときにequals実装すれば良い!!)
3. 完全に交換可能である
1.の特徴で出てきた代入はつまり、中身を交換すること。
変更するときには必ず「交換」するしかなく、
オブジェクトの一部だけを交換することはできない。
亜大オブジェクトにはどんなメリットが有るのか?
1個1個オブジェクトを作るのは面倒くさい。
だが…。
- 表現力が増す
どんな値を持つのか?を明示できる - 存在しない値を存在させない
値オブジェクトで「50文字より大きければException」と指定ができる - 誤った代入を防ぐ
誤った代入がコンパイルエラーで防げることも
エンティティとは
「属性ではなく同一性で識別されるモデル」
つまり、 値オブジェクトの逆
属性ではなく同一性(ID)によって識別されるモデルであり、ライフサイクルを持っている。
可変
一つの要素だけを変更することができる。
changeNameのようなメソッドを作ることができる。
同じ属性でも区別される
姓名が同じUserは同じ…ではない。
つまり 値オブジェクトとしては同じだが、エンティティとしては別。
IDという値オブジェクトをエンティティに追加して、同姓同名を識別できるようにする。
同一性を持つ
名字が変わったら別人…ではない。
こちらも、IDを追加すれば同一人物であることを証明できる。
IDだけをequalsで見て同一性を確認できる。
サービス(ドメインサービス)とは
ユーザ名の重複を許さないというルールがあったとして、
それをUserオブジェクトに実装すると、Userが自分自身に対して重複チェックしに行くことになり違和感がある。
チェック用オブジェクトを作ると、チェック専用のUserという実在しない謎の人物が生まれる。
つまり、 エンティティに実装するのが自然じゃない処理 がドメインサービスに書くべき処理。
上記の場合だと、「UserService」を作って実装すれば良い。
リポジトリ
データの永続化を担うオブジェクトで、データ(エンティティ)を保存して再構築する。
UserRepositoryに切り出すことで、 ドメインサービスがスッキリして、ユーザ関連の処理に集中できる。
リポジトリにインターフェースを用意し、
リポジトリにインターフェースを実装させてテスト用にインメモリ保存のRepositoryを作ると、
テスト用のデータを用意することなしにテストができる。
アプリケーションサービス
(エンティティ、値オブジェクト、ドメインサービス、リポジトリを使って)ユースケースを表現する。
たとえば管理者は何ができるか?をそのまま示せば良い。
このアプリケーションサービスをControllerにDIして持たせれば、MVCに組み込める。
セパレートインターフェース
レイヤーをまたぐときにインターフェースを用意すると、例外を投げやすいらしい。
※この辺はその場でもググってもそこまでピンとこなかったので、また勉強しないと…。
ファクトリ
ユーザを作るときにどこにその処理を書くか?問題。
ユーザの作り方をユーザが知っているのはおかしい。
その処理を書くのがファクトリ。
Repositoryに書くこともできるが、DBを使わない採番の場合は違和感がある。
例えばAPIで採番して、DBで永続化するとき等。
DDDとDBのオートインクリメント(IDの自動採番)は相性が悪く、
利用するのが無理ではないが一時的にIDがない状態を作らざるを得ないとのこと。
トランザクション
たとえば同一のメアドを持つUserの作成を許可しないとした場合、
同時実行した際に重複チェックをすり抜けないようにトランザクションを使う必要がある。
これは、ユニークキーを付けて重複チェックをしないという実装も可能ではあるが、
消してしまうと 重複を許さないかどうかがソースコード上でわからなくなってしまう。
制約がソースコード上で明示されていること こそがDDDの重要なポイント。
C#でいうとusingを使うと、整合性が求められるスコープであるということを明示的に示せるそう。
JavaのSpringフレームワークでも示せる。
ないときは自分でそういうメソッドを作るとよい。
ユニットオブワーク
整合性を示したいときに使えるらしい。
リポジトリを増加させるたびにユニットオブワークに修正が入ってしまうので、
ジェネリクスを使って特定エンティティのリポジトリを取得できるようにするといい。
集約(アグリゲート)
集約は意味のかたまり。
そもそもDDDはgetterを避けるべき。
Circleクラスの中に作成者のidが必要なのであれば、
Userクラスの中にcreateCircleメソッドを書けばgetterを使わないで済む。
Circleに所属できる人数が決まっているのであれば、それはCircleクラスに書く。
そうすると、所属できる人数が変わったときも楽勝。
デメテルの法則を意識し、「.」が2つ以上にならないようにする と良い。
(別のオブジェクトを経由して値を要求しないように作れば良い)
たとえばサークルに新しい人が参加する場合、
Circleクラスの中でUserの追加をすると、CircleクラスにUserに関する処理が多くを占めることになってしまう。
しかし、意味のかたまりをどう分ければ良いのか?
集約は変更の単位として考えれば良い らしい。
意味のかたまりごとにRepositoryができることになる。
ただ、この単位で区切っただけではCircleクラスからUserクラスの中身を変更できてしまう。
変更してはいけないものは、できないようにしたほうがいい。
エンティティごとCircleクラスで持つのではなく、
エンティティのなかのUserIdだけを持つようにすれば、IDは変更不可なので必然的に変更不可になる。
もしくは、クラス内のgetterをprivateにする。
その場合は、Repositoryでプロパティアクセスできず困ってしまう。
C#の場合はNotificationパターンを使うといいらしい。
一方Scalaは特定のインターフェースのみにprivateの除外をするような形で開放できるそう。(すごい!
ディレクトリ構成
ディレクトリ構成のイメージを説明していただきました。
こういうの、地味に悩むポイントだから助かる…!
アーキテクチャ
DDDと一緒に語られやすいアーキテクチャについても説明していただきました。
レイヤードアーキテクチャ
4層に分ける。
- Presentation:UserController
- Application:UserApplicationService
- Domain
- Infrastructure
依存の方向は下方向。
ヘキサゴナルアーキテクチャ
あんまり五角形に意味はないとのこと。(これ地味に驚いたw
UIを外側、DBを内側として見るのではなく、
UIとDBを外側、Applicationを内側としてみるというのがレイヤードやオニオンと異なる点。
五角形の対角に注目し、内側と外側とをつなぐApplicationServiceやRepositoryがあるのがミソらしい。
オニオンアーキテクチャ
レイヤーと大して変わらない。(えっ
ドメイン駆動設計そのもので、ヘキサゴナルアーキテクチャの内側。
クリーンアーキテクチャ
ヘキサゴナルアーキテクチャの外側。
ヘキサゴナルアーキテクチャ=クリーンアーキテクチャ+オニオンアーキテクチャ
いずれにせよやりたいことは一つで、 ビジネスロジックの防衛をすること 。
フロントは変更されるが、ビジネスロジックは簡単には変更しない。
(ビジネスロジックは変わらないわけではないが、変わるときは一大イベント!)
ビジネスロジックを変えたときには外側(フロント)に伝播させたい。
流れ的にはレイヤードからヘキサゴナル(内側:オニオン、外側:クリーン)への変化。
ドメインとはなにか?
人の営みに境界線を引いて、問題を解決しようとする こと。
そしてこれこそがエンジニアの役割。
重要なのは、作ったものではなく、
業務に精通する人に話を聞いて、ソフトウエアに必要なものを蒸留することが必要。
それをもっと大切にしてほしいし、この知識を研ぎ澄ますこと自体がエンジニアリングである、とのことでした。
質疑応答
Q.どうやってドメインに切り分けるのか?
A.ユースケースとかユーザーストーリーとかちゃんとしたようなことはしない。
お客さんの話を聞いて、変な図を書いて整理したりする。
Q.複雑なクエリが必要なときどうすれば良いのか?
A.CQRS(コマンドクエリ責務分離)を使う。DDDを使うのは諦める。(!)
クリーンアーキテクチャを使うのが良いらしい。
CQRSとDDDを組み合わせるという意味なのだと思いますが、こちらが参考になりそうです。
終わりに
DDDにはすごく興味がありつつ、しかし今ひとつイメージがつかなかった状態から、
実装に関してはなんとなくイメージが湧いてきました。
成瀬さんもおっしゃっていましたが、
これを踏まえてエヴァンス本を読めば多少すんなり入っていけるのかなという印象です。
これから業務でDDDをやっていきたいと考えているので、
私は設計の立場ですが、実装観点でもアドバイスできるようにこれからエヴァンス本を読むチャレンジをしたいです。