この記事は
「ドメイン駆動設計入門」を読んでまとめている第7弾
前回記事はこちら
今回はアーキテクチャと軽量DDDと境界付けられたコンテキストについてまとめる
末尾にソリューション構成を書く
まとめている過程でみつけたとても分かりやすい記事(Qiita内リンク)
アーキテクチャ
アーキテクチャは知識を記述すべき箇所を示す方針(P317)
ドメインからルールが漏れ出すと、変更箇所が点在し、バグの原因となる
ルールがサービスに漏れ出ることを貧血症と呼ぶ
貧血症を予防する力を養うために、アーキテクチャを学ぶ
悪い例「利口なUI」
ロジックを知るUIを利口なUIと呼ぶ
利口なUIはアンチパターンである
UIの責務
UIの責務は入力(の解釈)と表示である
UIは利用者からの入力を受け取り、ルールに基づいて出た結果を表示することだけが責務である
ECサイトの例
たとえばAmazonなどのECサイトのUIは、買う/買ったものの注文金額を表示する
注文金額を算出する方法はルールであり、ルールはUIに書かれるべきではない
"注文金額"はECサイト内の様々な場面(画面)で表示される
それらの表示内容(注文金額)をUIが計算してはいけない
(ではどこに書くべきか?を定めるのが各アーキテクャはであり後述される)
- 注文確認
- 注文確定ボタンを押す画面
- 注文履歴一覧
- 過去に注文したリストが一覧で見れる画面
- 注文履歴詳細確認
- 注文した内容の詳細が見れる画面(何を何個買った)
レイヤードアーキテクチャ
本書で扱ってきたサンプルはレイヤードアーキテクチャである
-
プレゼンテーション(UI)
- ユーザーインターフェースとアプリケーションを結びつける
- 表示と入力の解釈をする
- WebもCLIも入力のひとつ
- MVCフレームワークのコントローラなど
-
アプリケーション
- ユースケースを実現する進行役
- ドメインを取りまとめる
- ドメインオブジェクトの直接のクライアントである
- ルールは記述しない(ドメインに書く)
-
ドメイン
- 問題を解決するルールや知識を書く
-
ファクトリやリポジトリのインターフェースもここに書く
- まとめその3で依存関係のコントロールについて触れた
-
インフラ
- アプリケーションのためのメッセージ送信
- ドメインの永続化・再構築
ヘキサゴナルアーキテクチャ
ゲーム機をTVやコントローラにつなげるように、つながれば動くイメージ
TV出力つながった先は液晶TVでもタブレット出力でもよい
コントローラは純正品でもサードパーティーのジョイコンでもよい
つながれば動く
ポートアンドアダプタとも呼ばれる
入力をプライマリポート/プライマリアダプタ、
外部に対してふるまうことをセカンダリポート/セカンダリアダプタと呼ぶ
class UserAppService
{
// セカンダリポート
private readonly IUserRepository repository
// Updateメソッドはプライマリポート
// Updateメソッドを呼ぶクライアントはプライマリアダプタ
public void Update(UpdateCommand command)
{
// 処理がセカンダリアダプタにうつる
repository.Save(user)
}
}
レイヤードアーキテクチャは層をわけるところまでしか言及していない一方、
ヘキサゴナルアーキテクチャはインターフェイスの考えも入っている
・・・と書いてみても、正直よく理解できていない
今日においては、インターフェイスをつかって依存関係の逆転をすることは当たり前に行われているので、両者に違いはないとのこと。。。
クリーンアーキテクチャ
ヘキサゴナルアーキテクチャはアダプタとポートがつけ外し可能ですよ、まで。
クリーンアーキテクチャは具体的な実装方法が明示されている(よく見る円の図の右下に書かれている)
円の中心にはドメインのルールが記述されたオブジェクトがあり、
詳細は円の外側に追いやられている
依存の矢印は円の中心に向かっている
アーキテクチャまとめ
いずれにせよドメインを隔離し、ドメインに対して依存すればよい
ドメインに重要なルールをおまかせする
そしてアーキテクチャを考えるときは全体を見ず、局所的に関係性を考える
あれもこれもではなく、近くのオブジェクトと話すようにする
そうすれば矢印は遠くまでいかず、依存も最小限に収められる
アーキテクチャは方針であり「何をどこに書くか」の指針である
ソフトウェア特有の事情からドメインオブジェクトをうまく隔離できるアーキテクチャを選択する
軽量DDDにならないために
トンカチを手にすると目の前のものが釘に見えて仕方なくなる(P344)
ここまでに学んだ値オブジェトなどの概念は、あくまで道具でありパターンである
一方、ソフトウェアの目的は課題を解決することである
パターンを使いたいがために、課題解決への道を誤ることがあってはならない
トンカチを持った子どもよろしく、DDDを学んだ開発者がすべての課題に対してDDDを当てはめようとすること(パターンに固執すること)を軽量DDDと呼ぶ
ドメインエキスパートと対話をする
課題は現場にある
課題を知るのはドメインエキスパート(そこで働く人)である
ドメインエキスパートの言葉を鵜吞みにせず、対話を通じて課題の解像度を上げていく
本当に解決すべきことは、最初の課題とはずれたところにあるかもしれない
ユビキタス言語をコードに使う
ドメインエキスパートが「名前の変更をしたい」と表現しているところは
UpdateName
よりChangeName
をつかうことで
翻訳のコスト、解釈のすれ違いを減らす
class User
{
// good 名前の変更:ドメインにとって自然な表現
void ChangeName(UserName name)
{
// ...
}
// bad 名前の更新:DBを意識した技術的な表現
void UpdateName(UserName name)
{
// ...
}
}
境界付けられたコンテキスト
境界付けられたコンテキスト=ドメインの国境のようなもの
同じ言葉でも意味するものが異なることがある
あるいは同じ言葉でも区別したほうがいいときがある
その場合は、意味別にパッケージをわける
文脈ごとにパッケージをわけることは、システムが大きくなるほど効果的
コンテキストマップ
もとは1つであったUser
を分割することで、ドメイン全体にわたるユーザーを把握することが難しくなる
また、ある文脈のUser
を変更したときに、ほかの文脈も変更しなければならない場合がでてくる
たとえば、識別子をUserId
からUserMailAddress
に変更した場合などは、関連するパッケージを開発する部隊にその旨を周知する必要がある
テストが最後の砦
修正内容が伝達されなかったときにソフトウェアを守る最後の砦がテストである
ソリューション構成
Visual Sudio でコードを書き出すときにまずやるのがフォルダ構成を考えること
ドメイン
- Core
- Models
- Users
- User: 集約ルート
- UserId
- UserName
- IUserRepository
- IUserFactory
- Circles
- Users
- Services
- UserService: 重複確認など
- Shared
- ISpecification
- Models
- Authenticate
- Models
- Users
- User
- Users
- Services
- UserService: パスワード認証など
- Models
アプリケーション
- Application
- Users
- UserApplicationService
- UserGetCommand
- UserGetResult
- UserCreateCommand
- UserData: データ転送用オブジェクト(ドメインオブジェクトのメソッドを安易に呼び出さないための措置)
- Circles
- Users
インフラ
- EF
- Users
- EFUserRepository: ドメインにあるインターフェイスを実装する
- EFUserFactory: ドメインにあるインターフェイスを実装する
- Circles
- Users
- InMemory
ドメインとアプリケーションを同じプロジェクトにする
ドメインのクライアントをアプリケーションに限定するために、その2つだけを同一プロジェクトにまとめる方法もある
- プロジェクト
- Application
- Domain
- インフラ
- プレゼンテーション
全体のまとめ
ドメイン駆動について少しは理解が深まった
ふだん使っている C# WPF (Prism Template)ではどう区分けするか実際にやってみようと思う
あと、Qiita Jobs のアンケート答えたら Amazon 5,000円券もらえたので、本家のエバンス本を買いたい・・・と思ったけど5,000円じゃ買えなかった orz