ChatWork Advent Calendar 2017の10日目の記事です。
こんにちは。かとじゅん([Twitter:@j5ik2o]) です。
何を書こうかと悩んだのですが、社内で意見を聞いたところ、やはりDDD関連がよいとなりました。
この記事も、もう四年前ですっかり古くなりました。最近どういう観点で実践しているかまとめてみます。(DDD初級者という方は、まず上の記事を読むことをお勧めします)
DDDを実践するにあたっての個人的な問題点は2つあります。ひとつは、「いきなりドメインモデルを作ることができない」という問題。もうひとつは、ドメインモデルを作り上げても実装コードに役に立つ振る舞いが思いつかず、いわゆる「ドメインモデル貧血症*1」になりやすいという問題です。このような問題は、僕がコミュニティで関わった多くのエンジニアから耳にします。今日の記事はこの二点について考えてみましょう。相変わらず、長編なので時間があるときにお読みください(笑)
「いきなりドメインモデルを作ることができない」については、おそらく何のためのドメインモデルか、その目的や目標が明確でないというのが原因と考えています。ドメインモデルの戦略には選択と集中があり、ドメインの意図を伝える役割があれば役に立つモデルといえます。(たとえば、「便利な機能であってもそれをさせない理由は、ビジネスルール上不完全なオブジェクトが作られてしまうから」など)そういった意図を何から導けばよいのかという問題があります。つまるところ、システム開発する以上、ドメインモデルの根拠を「要件」に求めることは必然ではないでしょうか。これは当然といえばそうですが、そのプロセスに具体性を求めたかったので、リレーションシップ駆動要件分析(RDRA)からヒントを得ました。
また「ドメインモデル貧血症」の問題に関しても、独りでに動作しないモデルなので振る舞いを付けないというメンタリティがあり、ドメインオブジェクトがただのデータとして扱われることが多いように見受けられます*2。リッチなドメインモデルをイメージしていても、実装コードに落とすと、表現力が貧弱になってしまうのは残念なことです。加えて、DDDは分析と設計で単一モデルを採用するというわけですので、これではスタート地点からつまずいていると言わざるを得ません。この点についても、ユースケースを根拠に振る舞いを割り当てるオブジェクトを見つけていくICONIXという手法を試してみました。お勧めの書籍は、以下です。
蛇足ですが、8月末にOAuth2認可機能をクローズドβで提供し、現在オープンβ期間中です。こちらのプロジェクト(以下、Astraeaプロジェクト)は、RFC 6749に準拠した認可サーバをScala+Akkaで実装し、上記2つの手法をプロジェクトや自社の状況に合わせて部分的に取り入れました。(APIドキュメントはこちら)。このプロジェクトの事例も織り交ぜながらまとめます。
なお、RDRAとICONIXの概要はこちらのスライドで確認できますので、よかったらご覧ください。
RDRA
RDRAは、網羅的で整合性のある要件定義を行うための手法です。大量のドキュメント=要件定義?などの皮肉を冗談で言ったりすることがありますが、そういうものではないです。要件定義とは、対象を分析・定義することで要件を固めていく作業のことで、ドキュメントはそれらの知識を共有するための手段という位置付けになります。基本コンセプトはスライドのほうに記載があるのそちらをご覧ください。
要件定義の基本的な考え方
スライドにもありますが、RDRAの要件定義では、以下を基本的な考え方としています。実際の要件定義の作業では、これらの考え方に沿ってまとめていきます。*3
- システム価値
- システムと関係を持つ対象とそこからの要求をとらえる
- システム外部環境
- システムを取り巻く外部環境を明らかにする。
- システム境界
- システムの外部と内部の境界を明らかにし、システムの範囲を明確にする
- システム
- システム内部の機能とデータ構造を明らかにする。
あと、一点だけ補足があります。RDRAでも表現力ある図を使います。以下に説明するプロセス内で実際にモデル図を作成しましたが、詳細すぎるものは抜粋として箇条書きで掲載しています。ご容赦ください。
システム価値
システム価値では、システムと関係を持つ利害関係者や外部システム、およびシステムに対する要求を洗い出します。有効なシステムを開発するためには、システムの価値を把握する必要があり、価値を明確にするためにはシステムに関わる人を把握する必要があります。システムの価値を決める対象が明らかになり、要件定義をスタートできるというものです。
Astraeaプロジェクトでは以下となりました。
- 価値・役割
- ChatWork上でOAuth2認証・認可を実現すること
- 関係者・組織
- システムに関わるのは、顧客組織、その組織内の管理者(組織管理者)、リソースオーナー(CWユーザー)、OAuth2クライアント開発者(以下、クライアント開発者)など
- 外部システム
- 外部システムには、ユーザーエージェント、OAuth2クライアント(以下、クライアント)、現行のPHPシステムなど
- 抱える問題・課題
- 現行APIトークンより、安全な認証・認可の基盤を実現すること。
- 実現する要件
- 組織管理者の管理下のもと、自由にクライアントを開発でき、業務効率化を図ることができる
さて、いきなりOAuth2に対応してよと上司から言われてもとまどいますよね…。まず、RFCを読むことは必須ですが、それ以外にプロジェクトの全体像を抑える必要があります。このように全体像を明確にすることで、認識のズレを見つけて早期に是正できるのではないでしょうか。
コンテキストモデル
次に、システムへの要求を発生させる元となる利害関係者・外部システムを洗い出すために、コンテキストモデルを作成します。開発時・運用時の両面で考えます。ここで洗い出したアクターはヒアリング対象になりえます(実際に会ってヒアリングできない場合は、代理かペルソナで想定を作る)。
Astraeaプロジェクトでは以下のようになりました。
- 関係するシステム
- 認可管理ツール(改修対象, PHP)
- 認可APIサーバ(開発対象, Scala) --> 僕のチーム
- 認可Webサーバ(開発対象, PHP)
- リソースサーバ(API)(改修対象, PHP)
- クライアント(開発対象外)
- 利害関係者
- 顧客組織側
- 組織管理者
- 組織内のクライアント開発者
- 組織内のリソースオーナー
- 開発側
- 認可APIサーバチーム
- 認可Webサーバ/リソースサーバチーム
- 現行PHPチーム
- インフラチーム
- 顧客組織側
今回は新規開発でしたが、現行のPHPシステムとどう統合するかは技術的にそれなりに難易度があります。認可サーバの機能をすべてScalaで実装するというプランもありましたが、既存の認証機能との兼ね合いで、認証・認可(ログイン画面とコンセント画面)の部分はPHP側で実装した方がよいという結果になりました。このようなモデル図を使って線を引けば、どこのシステムがどことつながるか明確になります。そのことによって、早い段階でチームごとの責務を明確化し、インテグレーションプランについて議論する機会が持てました。人はコンウェイの法則になかなか逆らえないので、これはよいプラクティスだと感じました。
要求モデル
要求モデルでは、各アクターが持つ機能要求・非機能要求を洗い出します。また、要望・要求・要件は以下のとおり明確に区別します。
- 要望
- こうできたらよいなという思いつきレベル
- 要求
- 検討対象として合意されているもの
- 要件
- 開発対象として合意されているもの
開発者は機能に関心が向かいがちですが、機能の根源となる要求を整理することは整合性の取れたシステムを作るためには不可欠でしょう。
Astraeaプロジェクトでは以下となりました。これはほんの一部なので実際にはもう少し細かい要求があります。
- 組織管理者
- OAuth2を制限・管理したい(セキュリティ上の観点)
- 組織としてOAuth2を利用するか決めたい
- ...
- ...
- クライアント開発者を制限したい
- ...
- 登録または運用されているクライアントを把握したい
- ...
- 組織としてOAuth2を利用するか決めたい
- OAuth2を制限・管理したい(セキュリティ上の観点)
- クライアント開発者
- OAuth2を使ってクライアントを開発・運用したい(利便性の観点)
- クライアント開発者になりたい(組織管理者の許可を簡単に得たい)
- ...
- クライアントを効率的に開発したい
- ...
- クライアントの運用を管理したい
- ...
- クライアント開発者になりたい(組織管理者の許可を簡単に得たい)
- OAuth2を使ってクライアントを開発・運用したい(利便性の観点)
- リソースオーナー
- クライアントを利用・管理したい(利便性の観点)
- CWアカウントでクライアントの利用したい
- ...
- 利用中のクライアントを管理したい(スコープの把握や認可の失効など)
- ...
- CWアカウントでクライアントの利用したい
- クライアントを利用・管理したい(利便性の観点)
アクターが明確になりましたが、一般的なWebサービスでは実際にヒアリングすることが難しいでしょう。アクターの代理として社内で顧客に近い立場の人に参加してもらい議論しました。実際は、このモデル図について議論したわけではなく、ユーザーストーリーマッピングを行い、あとでこの図を整理しました。ここで重要なのが、組織管理者としては"OAuth2機能を制限・管理したい"という要求に対して、クライアント開発者は"クライアントを開発・運用したい"という一見すると相反する観点です。この線引きは、簡単ではありませんでした。どういう要望を受入るか方針が明確でないまま、開発プロセスを実行すると仕様や実装に悪影響を与えることは自明です。最終的に、要求の優先度・重要度・矛盾・過不足などを整理しながら、最初のスコープを決めていきました。先にも述べましたが、要求モデルとこの後に述べるユースケースはユーザーストーリーマッピングと重複するところが結構ありますので、そのチームで最適なメソッドを使って議論するとよいでしょう。
システム外部環境
システムを取り巻く外部環境として、業務フローもしくは利用シーン、および関連する概念をモデル化します。システムはビジネス環境の中で価値を生み出すため、環境に大きく依存し、同じようなシステムでも、その環境が違えば要件も変わります。システムを取り巻く環境を決めることはシステム要件を決める上で決定的な役割を果たします。
利用シーン
汎用的に利用されるツールやサービスなどの提供を目的にする場合は、利用シーンを利害関係者別に整理します(特定の業務であれば、業務フローを整理しますが今回は割愛します)。利用シーンでは、システムがどのような場面で利用され、どのような価値を生み出すかをとらえます。
Astraeaプロジェクトでは以下のようになりました。全部書くと多いので抜粋です。こちらも実際は図を作成しました。
- 組織管理者
- OAuth2を運用するかどうか決めさせたい
- 組織内の運用ポリシーに併せて、認可機能を使うかどうか決めたい
- クライアント開発者を制限したい
- クライアント開発者の申請ベースにするか、自動承認とするか
- クライアントを管理したい
- OAuth2を運用するかどうか決めさせたい
- クライアント管理者
- クライアント開発者になりたい
- 申請が有効な場合は、組織管理者の承認を必要にしたい
- 申請中の状態が確認できるようにしたい
- クライアントを開発・運用したい
- OAuth2の仕様に従いクライアント情報を事前登録できること
- クライアントシークレットは、自動生成したい。変更時も再生成できること
- クライアント開発者になりたい
- リソースオーナー
- クライアントを利用したい
- OAuth2のフローに従いリソースオーナーはクライアントに認可を与えることができること
- 認可を与えたクライアントを管理したい
- 認可を与えたクライアントの一覧を管理画面から確認でき、必要であればその認可を失効できること
- クライアントを利用したい
このような利用シーンは、ユースケースの裏付けとしても有益です。裏付けが不十分なものを理解するのも実装するも、難しいでしょう。また、開発者として、その利用シーンに妥当性があるか気にした方がよいです。開発プロジェクト自体のコストパフォーマンスを考えると、利用者・提供者双方にとって、価値のないものを最初から排除することが最終的な生産性に最も寄与するからです。実装が始まってからでは取り返しがつきにくいので、この段階で認識を合わせることは、僕らの安定した日常生活を実現するために欠かせません(笑)。
概念モデル
対象となる業務や利用シーンでどんな用語や概念が使われているか整理します。要件を合意するためには、共通の概念理解が欠如していては不可能ですので、このような作業をするようです。言わずもがな、同じ用語でも文脈が違えば、目的が異なることもあります。議論がかみ合わない場合は、まず概念の整理をした方がよいでしょう。
Astraeaプロジェクトでは以下となりました。こちらも簡単な図を描きました。詳細は掲載できないので箇条書きで掲載します。
- 代表的な集約(グローバルなエンティティ)
- クライアント
- 予約済み認可
- 認可
- 認可コード
- 定義済みスコープ
- 代表的な値オブジェクト
- 組織ID
- アカウントID
- クライアントスコープ
- リダイレクトURI
- アクセストークン
先に洗い出した業務フローや利用シーンが明確になっていれば、この作業はさほど苦労しないでしょう。おそらく、概念モデルの候補はいくつか見つかっているはずです。この概念モデルは、DDDのユビキタス言語に対応すると考えてよいでしょう。
Astraeaプロジェクトの場合は、ドメインの中心がOAuth2ですので、RFCの用語を用いることが多くなりました(当然ですがエンジニアもエンジニアではないメンバーもプロジェクトの初期にOAuth2の知識はある程度インストールする必要がありました)。たとえば会計システムを作るのであれば、すでにその業界の用語が見つかるはずで、それらから概念モデルを洗い出せばよいでしょう。 また、当初は、組織・開発者・リソースオーナーというドメインモデルがありました。しかし具体的にインテグレーションを進めていく中で、実装上不都合があったためサブシステム間の責務を見直し、認可APIサーバでは値オブジェクトのみとなりました。このような意思決定を行う場合も、根幹となる概念モデルの統一的な認識は必要になるでしょう。
システム境界
システム内部と外部の境界に位置するものとして、ユースケース、画面・帳票、プロトコル・イベントなどを明らかにしていきます。
ユースケースモデル
システム化する範囲はどこまでか、システムとの設定はどのようになるかを明確にします。それを明確にするため、ユースケースモデルを作成します。
Astraeaプロジェクトのユースケースは膨大なのでここでは一例のみ(例外コースもありますが省略します)を紹介します。
- トークンエンドポイントの基本コース
- リソースオーナー(UA)は、自動的にクライアントへリダイレクトする
- クライアントは、受け取った認可コードとリダイレクトURI(オプション)を検証する
- クライアントは、認可コードをもとに、認可APIサーバにトークンリクエストを送信する
- 認可APIサーバのトークンエンドポイント(B)
- は、クライアント(E)を認証する(C)
- は、認可コード(E)をリポジトリから読み込み・削除する(C)
- は、認可コード(E)を検証する(C)
- は、認可コード(E)のリダイレクトURIとトークンリクエストのリダイレクトURIを検証する(C)
- は、アクセストークン(E)を生成する
- は、クライアント(E)にトークンレスポンスを返す(C)
- クライアントは、取得したトークンレスポンスのアクセストークンをもとに、リソースサーバにユーザー情報リクエストを送信する
ユースケースは、概念モデルを使って簡潔に記述して、チーム内で認識の違いがないように整理します。ちなみに、(B)はバウンダリ, (E)はエンティティ, (C)はコントロールです。これらは、RDRAには登場しません。ICONIXのほうに出てきますが、ドメインモデルとそれに関係する振る舞いを整理する際に、目安になります(詳しくは後述します)。(画面/帳票モデル、プロトコル/イベントモデルは不要だったので作りませんでした。すべてのモデル図を描けばよいということはありません。目的に応じて選びましょう)
システム
最後にシステムの内部構造を明らかにする、データモデルもしくはドメインモデル、機能モデル、ビジネスルールを作成します。
Astraeaではドメインモデルだけをまとめ上げました。表などを作って保持する属性などをまとめたりしましたが、この段階ではもうコードを書いた方がわかりやすかったので、議論はコード上が中心になりました。たとえば、認可集約のサブ型であるコードフロー(CodeFlowAuthorization)は、認可コードを生成するためのファクトリメソッドを持つようにしました。CodeFlowAuthorization以外にもImplicitFlowAuthorizationもありますが、このオブジェクトにはそういうメソッドはありません。インプリシットフローは認可コードを発行しないからです。
sealed trait Authorization extends Aggregate { // ... override val id: AuthorizationId val flowType: FlowType val clientId: ClientId val resourceOwnerId: ResourceOwnerId val scopes: VerifiedAuthorizationScopes val createdAt: ZonedDateTime val updatedAt: Option[ZonedDateTime] val revokedAt: Option[ZonedDateTime] val revocationReason: Option[RevocationReason] } object Authorization { // 認可コードフローにとしての生成義務を果たすファクトリ // 認可コードフローの不変条件を破る生成処理はできない。 // 生成に成功すると常に正しい状態のオブジェクトが作られる。 def ofCodeFlow(...): CodeFlowAuthorization = CodeFlowAuthorization(...) } case class CodeFlowAuthorization private (...) extends Authorization { override val flowType: FlowType = FlowType.AuthorizationCode // AuthorizationCode#applyでは、不変条件を破るオブジェクトが作られる可能性があるため // 正しい状態を維持したCodeFlowAuthorizationからしか作れない。 def newAuthorizationCode(...): AuthorizationCode = AuthorizationCode(...) }
ユースケースで洗い出した、コントロールをアプリケーションサービスへ、トランザクションスクリプトのように割り当ててしまうとこのようなデザインになりません。OOPのメンタリティが中心であるチームであれば、ひと塊の概念を同じオブジェクトで理解できた方が自然でしょう。DDDのファクトリパターンとしても、ほかのオブジェクトの生成に関わる、密接なオブジェクトにファクトリメソッドを配置することがあります。あるオブジェクトが持つ属性やルールが、別のオブジェクトを生成するうえで支配的である場合、このような選択を行う場合があります。このようなオブジェクト間の関係性を作り上げることで、ドメイン上で関心の近いものは高凝集したり、関心の遠いものは低結合にしたりします。
ICONIX
前述のような振る舞いをどのドメインオブジェクトに割り当てるかはRDRAだけでは難しいと感じました。それに対しては、ICONIXのアプローチを借りました。
プロセス
ICONIXのプロセスは、簡単にいうと以下のようなものです。モデル図などは、スライドの資料をご覧ください。
- 要件定義
- ドメインモデリング
- ユースケースモデリング
- 分析/概念設計/テクニカルアーキテクチャ
- ロバストネス分析
- 設計/コーディング
- シーケンス図を作成
- テスト/要求の追跡
ドメインモデリングとユースケースモデリング
要件定義のドメインモデリングやユースケースモデリングはRDRAでも同様にありますが、ICONIXはユースケースをドメインモデルにどのように紐付けるかについてフォーカスしている気がします。要件定義の段階では、ドメインモデルの動的な側面(振る舞いなど)は、まだ考慮しません。あくまでドメインモデルの候補を探して議論することが目的ですので、この時点のモデルは不完全なものと仮定しています。また、ユースケースを洗い出す前に、ドメインモデルを考える点もRDRAとよく似ています。それはドメインモデルの用語(ユビキタス言語)でユースケースを表現するためです。ただ、ICONIXではドメインモデルをどこから導くかは明示されていなかったので、RDRAのプロセスと併せて考えるとよいでしょう。
そのユースケースにはユースケースのタイトル部分と具体的な内容を含むユースケース記述があります。この工程ではユースケース記述まで含みます。ICONIXが特徴的なのは、「動詞+れる/られる/できる」などの指示的表現ではなく、「〜する」などの叙述的表現を使うことです。このようなあいまいな表現は、ソフトウェアの重要な振る舞いを隠してしまい、結果的に貧血症へ近付いてしまうわけです。 たとえばログインするユースケースであれば、「ユーザーがユーザー名とパスワードを使ってログインできる」ではなく、以下のようなユースケースを考えます。
- ユーザーはユーザー名とパスワードを入力して、「ログイン」ボタンをクリックする
- システムはユーザー名からユーザー情報を取り出し、パスワードをチェックする
- そして、ユーザーはシステムにログインする
などです。 つまり、ログインに含まれる手順を示すことで、重要な振る舞いを抽出します。少なくとも、「クリックする」・「取り出す」・「チェックする」という振る舞いはユースケースから機械的に抽出可能です。無論、「クリックする」はUI要素の振る舞いで、「取り出す」はI/O責務ではないか推論はできますが。
ロバストネス分析
次のロバストネス分析では、BCEステレオタイプ*4を意識しながらユースケースをパターン化します。ロバストネス分析から得られるロバストネス図の例はこのページをご覧ください。 あらためて、BCEステレオタイプの意味を以下に示します。
- バウンダリ(Boundary)
- システム外部との境界。画面であったりAPIであったりのインターフェイスです。
- コントロール(Control)
- 制御を行う責務。ソフトウェア機能です。つまり処理。
- エンティティ(Entity)
- 情報を保持する責務。ドメインオブジェクト。
Astraeaプロジェクトでは、最初、ロバストネス図を何度か描いていました。しかし、慣れてくるとBCEを意識したユースケース記述から実装に落とせてしまったので、基本的に描いた図自体は使わなくなりました。図自体よりは、その図が示そうとする考え方のほうが重要ではないでしょうか(苦笑)。
シーケンス図
プロトコル仕様の認識を合わせるために、シーケンス図をよく描くことがありますが、このシーケンス図は全然違う目的で描かれます。コントロールをどのエンティティに割り当てるか見極めるために作ります。スライドはこのあたりから。ステレオタイプのコントロールと聞くと、コントローラオブジェクトをイメージしますが、ICONIXではエンティティへのメッセージとしてとらえます。そのメッセージを受け取ったエンティティが振る舞いを起こすことになるわけです。そういう意味では振る舞い自体はエンティティが起こすので、コントロールは振る舞いではなくその振る舞いへ与える入力になります。
Astraeaではロバストネス図と同じ理由で描いてません。抽出したコントロールをどのオブジェクトに割り当てるか、というエッセンスに注目しました。これには慣れが必要ですので、何度かモデル図を描いて練習した方がよいです。また、たとえ慣れていても要件が複雑で議論が紛糾するドメインモデルは、そのテーマに絞ってモデル図を描くことをお勧めします。
どうやってドメインモデルの動的な側面を分析するか
ロバストネス分析からシーケンスを起こす作業の何がよいか。1つ目は、バウンダリであるUI要素とエンティティを分離できることです。2つ目は、UI要素を区分したのちにエンティティ群へ注意を払いながら、コントロールの適切な割り当て先を見つけられることです。そうすれば貧血症になりにくいという話です。ただ、このようなプロセスを踏んでもドメインオブジェクトが貧血症になる場合があります。それは、ドメインモデルの静的な側面にしか注目していないのが原因です。この考えのままだと、ほとんど振る舞いがエンティティに割り当てられず、そこにはsetter/getterのみが存在というものになりやすいでしょう。特に、人間や動物以外は自律的に動作しないという決めつけのメンタリティがあると邪魔になります。電話帳オブジェクトは何かするでしょうか?一般的に、情報を保持するだけで何もしません。一方、サーモスタットは判断を行い、制御信号を送信します。電話帳オブジェクトには本当に振る舞いは必要ないのでしょうか?これは分析でどのような責務を与えるかで変わってきます。Alexander先生は、やかんには"水の沸騰を知らせる"という責務があると考えたそうです。なるほど!つまるところ、OPPはプログラミング言語自体より、このような分析が難しいということなのです。DDDにおいても、オブジェクトパラダイムを主軸に置いているので、そのオブジェクトに責務をどう与えるかは重要なテーマです。余談ですが、責務がよくわからないという方は、オブジェクトデザインという古典(絶版です…)を読むとよいです。
あとから振る舞いを割り当てる適切なオブジェクトを探す
これまで説明したとおり、初期の段階で振る舞いがすぐ見つからない場合もあります。たとえばエンティティとして以下のようなモデルがあり、ユースケースにコントロールはあるが、適切にドメインオブジェクトに割り当てられていない場合です。これは貧血症なドメインオブジェクトといえます。自口座から「出金する」・自口座へ「入金する」はアプリケーションサービスにないでしょうか。
// 口座オブジェクト case class BankAccount(id: Long, events: Seq[BankAccountEvent])
さしずめ、以下のようにアプリケーションサービスに記述されていることが多いでしょう。オブジェクトというより構造体に近いです。この程度なら大きな問題ではないでしょう。しかし何らかの理由によって構造が複雑になれば、知識が分散したり、ほかのオブジェクトが理解の邪魔をする可能性があります。そういう意味で、振る舞いをすぐに理解することが難しくなります。また、誤解されている方が多いのですが、アプリケーションサービスの責務はドメインモデルへのタスクの割り当てやその進行管理です。ビジネスロジックやルール自体はドメインモデルが持つので、このような振る舞いは本来の責務から逸脱するといえます。せっかくDDDをやっていても、この手法ではほぼトランザクションスクリプトと変わらないので、あまりお勧めできません。
// 口座アプリケーションサービス object BankAccountApplicationService { // 残高の計算方法はBankAccountにない!? def getTotalBalance(bankAccountId: BankAccountId): Try[Money] = BankAccountRepository.resolveBy(bankAccountId).map{ bankAccount => bankAccount.events.foldLeft(Money.zero){ (result, e) => result + e.money } } }
お勧めができないといったものの、現実問題としてはこのようなケースは身近に存在します。すぐに振る舞いに昇格できなくても、気付いたときに後からできればよいのではないでしょうか。僕は以下のように、そのオブジェクトの内部状態を暴露するような属性値には、長い名前をつけています。豪快に、カプセル化を破るという接頭辞がついています。
case class BankAccount(id: Long, breachEncapsulationOfEvents: Seq[BankAccountEvent]) object BankAccountApplicationService { def getTotalBalance(bankAccountId: BankAccountId): Try[Money] = BankAccountRepository.resolveBy(bankAccountId).map{ bankAccount => // すごい違和感!! // ユースケースの一部としてのコントロールをオブジェクトに送信している bankAccount.breachEncapsulationOfEvents .foldLeft(Money.zero){ (result, e) => result + e.money } } }
このようなコードを利用すると違和感を感じます。使えば使うほどコードの見通しも悪くなります。そういうときに、ドメインオブジェクトの振る舞いをレファクタリングするチャンスです。上記の例では、ありきたりですが以下のようなリファクタリングができるでしょう。このアプローチは、高凝集・低結合なドメインオブジェクトを作り上げていくときに、地味に効果があります。お勧めです。
// eventsは隠蔽できるならprivateでもよい。そうでなければ長いままにする。 case class BankAccount(id: Long, private val events: Seq[BankAccountEvent]) { // メソッド名には、ユースケースに立ち戻り適切なユビキタス言語を付ける def totalBalance: Money = events.foldLeft(Money.zero){ (result, e) => result + e.money } } object BankAccountApplicationService { def getTotalBalance(bankAccountId: BankAccountId): Try[Money] = BankAccountRepository.resolveBy(bankAccountId).map{ bankAccount => bankAccount.totalBalance } }
まとめ
長編になってしまいましたが、長々と読んでいただきありがとうございます(笑)。今回はプロジェクトのスケジュールの都合もあり、完全に型どおりには実践していません。ですが、肝心な考え方はある程度身についたと感じています。また、今までの感覚で実践すると大量にドキュメントができてしまう可能性がありますので、あくまで議論するためのモデル図を描くぐらいの感覚のほうがよいと感じました。
ここで紹介した手法は、古くから知られているものが多く含まれています。だからといって、使えないということではありません。そこからまた今の開発スタイルにあった考え方を導きたいところです。温故知新ですね!。今後も自分なりのスタイルを追求していこうと思っています。皆さまのご参考になれば幸いです。
ということで、明日のアドベントカレンダーも乞うご期待。
*1:詳しくはこちら参照 → http://bliki-ja.github.io/AnemicDomainModel/
*2:僕もそういうツイートをしたことがあります。その後あらためて考え直しました
*3:RDRAの要件定義を行う前に、プロジェクトの前提やゴールを明確にするためインセプションデッキを作りました。そうすることで、RDRAの要件定義もそれに合致したストーリーとして定義できるはずです
*4:Ivar Jacobson氏のObject Oriented Software Engineering: A Use-Case Driven Approachで登場します