昨日もDDDの話題を少ししたので、シナリオ→モデル→コードのサイクルについて身近な例を踏まえてネタを提供できないかと思った。何でもいいんだけど、鍼とか整体とかマッサージとか一度は行った経験あると思うので、そのドメインで考えてみるか。
実際は仕事に詳しい人を、ドメインエキスパートにした方がいいだろうけど、今回は自分でやってみる。
シナリオからドメインモデルを考える
ドメインモデルにでてきそうな概念を、ひとまず人モノなどのリソースから考えてみたい。
- 患者
- 施術師
- 施術方法
まずはこれぐらいから。
このドメインは、患者が施術師の時間を予約することが目的です*1。
簡単にシナリオを考えてみる。
- 患者が施術師の空き時間を予約できる。
- 患者が施術師に予約を要求(以下, 予約要求)する。
- 予約要求には、患者番号, 開始日時, 施術方法が必要。
- 施術方法には、マッサージ30分コース、マッサージ60分コースがある。
- 施術師が予約要求を許可した状態を予約という。予約は患者に通知する必要がある。
- 予約には、予約要求に含まれる情報の他に施術師番号が含まれる。
- 患者は施術師を指名できない。
- 患者が施術師に予約を要求(以下, 予約要求)する。
- 患者が予約をキャンセルできる。キャンセルは無条件にできる。
- 患者が予約を確認できる。
という具合。そもそもこのシナリオが使えるかどうか、という視点でドメインエキスパートとソフトウェアエキスパートが議論すべきだろうね。本来は。
図にしてみる
早速これを絵にしてみよう。別にクラス図じゃなくてもいいし、クラス図でも詳細に書ききる必要はありません。図に書ききれないドメインに関する制約などは声に出して補完しよう。図に書ききれない詳細はコードで表現されるべきです。つまるところ、図自体ではなく図が伝えようとする何かが大事なのです。そういう話は一部にでてきますので、興味ある人は読むといいかも。
ちなみに、この図はマークアップだけでUMLが書けるPlantUMLで書いてみた*2。
図中の、エンティティ(Entity)や値オブジェクト(Value Object)、リポジトリ(Repository)というキーワードは、DDDの第二部に登場する設計パターンです。
エンティティは、同一性や連続性を表すモデル。エンティティは属性を持ちますが、その属性が変化しても同一であることを保証する特徴があります。もっと簡単に言えば"見分ける対象になる概念"です。見分けるために識別子を持ちます。この例では、患者(Patient)と施術師(Practitioner)、予約(Reservation)がエンティティです。値オブジェクトは、値を説明することが目的のモデルです。同一性を保証することはありません。この例では、施術方法(CureMethod)と予約要求(ReservationRequset)が値オブジェクトです。
属性について若干説明します。 Patient#idは患者番号、Practitoner#idは施術師番号、Reservation#idは予約番号でそれぞれを見分ける=識別のために使います。 CureMethod#MASSAGE30はマッサージ30分、CureMethod#MASSAGE60はマッサージ60分を表します。
あとは関連の説明。エンティティにはリポジトリが必要です。なぜなら永続化されたエンティティへの参照を取得するために必要だからです。ということで、それぞれのエンティティにはリポジトリが対応づきます。
それぞれのエンティティの関連ですが、相互依存を回避するためにエンティティの識別子だけを持つようにしてみました。参照が欲しくなるのであれば、識別子を用いてリポジトリからエンティティへの参照を取得すればいいわけです*3。
コードに落とし込む
能書きはこのあたりで実際にコード例を考えてみましょう。上図はJavaで書いたのですが、コードは面倒なんでScalaで書いた。アプリケーションのUIのロジックなどから呼び出される想定で読んでよいと思います。 クライアントコードを書いたので、内部実装はどうなるのっては別のエントリにします((内部実装の話題として、テーブル設計どうなるの?とかSQLは誰が発行するの?って議論はみんな好きだと思いますが、ここではわざと書きませんでした。そこから考えるとドメイン駆動設計にならなくて、DOAになってしまうからです。テーブルはドメインモデルではありません。最初にドメインモデルを考えたら、次はそれに合わせたテーブル設計を考えます。(こういう話は新規開発ならそういう前提で設計できるけど、メンテナンスモードだとやりにくいよねってのは確かにありますが)))。
予約する患者から患者IDを与えてもらい、このドメインは患者オブジェクトを引き当てます。患者オブジェクトが希望日時と施術方法を元に予約要求を作成します。
患者オブジェクトが予約要求を作成するコード例
// 患者オブジェクトの参照を得る val patient = patientRepository.resolve(id) // 予約 val sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm") // 予約要求を作成する val request = patient.makeReservationRequest( sdf.parse("2013/05/11 16:00"), CureMethod.MASSAGE60 )
次は実際に施術師を探し、予約を確立させるコードを考えてみます。まず施術師の参照を管理しているリポジトリ(PractitionerRepository)に予約要求に応じれる施術師を検索します。そして、空きの施術師に予約要求を与えて、予約を確立させます。確立した予約は患者に通知されます。そして、予約は永続化されます。
予約要求に応じれる施術師を見つけ出し予約を確立させるコード例
// 空いてる施術師を探す val practionerOpt = practitionerRepository.findByAcceptable(request) val reservation = practionerOpt.map{ practitioner => // 施術師に確認させて予約を確定する val reservation = practitioner.accept(requet) // 確立した予約を患者に通知する。通知を受けたらReservation#idだけを保持する。 patient.receiveReservation(reservation) // 予約を永続化する reservationRepository.store(reservation) reservation }.get // 予約を返す
しかし、この知識は、アプリケーション層ではなくやはりドメイン層である気がしてきました。この振る舞いを患者にやらせるのも不自然な気がするし。実際には"受付"が行うのではないかな。そう思えてきた。受付(Reception)というドメインモデルが必要かもしれないですね。
受付が患者からの依頼を受けて、予約を確立させることができる
// 上記の振る舞いが行われる val reservationOpt = reception.makeReservationBy(request)
受付(Reception)は、振る舞いだけしか持っていないがサービスではないと考える。どの受付でも予約を受け付けれるなら、同一性の概念もいらない。値オブジェクトではなかろうか。いや、振る舞いしかないとしたら、やっぱりサービスかもしれないなぁ。というようなことをチームで議論すべき。
他にも予約の確認やキャンセルなども考えてみた。何れにしても受付に依頼するのが筋だろう。Patient#getReservationIdsは予約確立時に通知を受けた予約番号の集合です。
予約の確認とキャンセルの振る舞い
// 予約の確認 val reservation = patient.getReservationIds.map { id => reservationRespository.resolve(id) }.get // 予約を返す // キャンセル for { id <- patient.getReservationIds reservation <- reservationRespository.resolve(id) practitioner <- practiotionerRepoistory. reoslve(reservation.practitionerId) } { // 予約への参照(ID)を外す practitioner.removeReservationId(reservation.id) patient.removeReservationId(reservation.id) // 予約の破棄 reservationRepository.delete(reservation) }
一例ということで書いてみたが、まだ甘いところはたくさんありますね。例外処理とかどうするのかとか、予約を完遂した場合はどのように予約のライフサイクルは変化するのかとか、議論の余地はあります。 とはいえ、こんな風に知識を噛み砕いてモデルを掘り起こして、実装に紐づけていけばソフトウェアがいったいどんな問題や課題を解決するのか、というソフトウェアの核心部分が明白になります*4。
次はドメインモデルの実装を書いてみよう。
追記:
コメントいただいたので、返事として追記します。
Kentaro Matsumae http://j5ik2o.me/blog/2013/05/11/sinario-model-code/#comment-893794195
ところで、スマホアプリのようなclient-server型のシステムの場合、ドメインモデルは共通のものを使うべきか、別々にすべきか気になります。たとえば、client(スマホアプリ)側は、単純に予約日だけを入力/送信して成立すれば「OKです」と表示される程度のものの場合、client側ではPractitioner/PractitionerRepositoryあたりは意識する必要がなくなると思います。逆にclientでしか登場しないようなモデルも出てきたり・・?
そうすると、今回PlantUMLで書いたような図は、client用、server用、と別々になっちゃう気がするんですが、それはそれでシステム全体としてみたときの姿がぼやけてしまう、、という問題が起きるような気もする。 このへん、どうですかね?
今回考えたドメインモデルは、マッサージの予約を行う際に使えるものという想定でした。患者からみた場合、施術師は見えなくてもよいのでは?どうせ指名できないのだから。よい視点です。でも逆に受付からみたら、患者を施術師に割り当てないと予約の保証ができません。患者としては直接的に利用しないモデルになりますね。つまり、クライアントアプリケーションが患者しか利用しないという前提であれば、施術師を利用しないということでよい気がします。
あれ、ほんとかな?それ。
あー、ここまで書いてて思ったのですが、そうなると予約のモデルの形がクライアントとサーバで異なってしまうことになる(クライアントの予約には施術師IDがない)ので、それはよろしくないとおもった。この時点でドメインモデルの意図が変わってしまう。不要だと思ったモデルを排除することで、それに依存している必要なモデルまでが意図せず変わってしまうのはよくないですね。それにドメインモデルはアプリケーションの実装を意識すべきではないよな。ということで、はやり基本的には同じドメインモデルをみるべきではと思います!
境界づけられたコンテキスト
若干トピックから逸れますが、モデルの言葉が有効な範囲というのが決まっています。それを"境界づけられたコンテキスト"(以下、コンテキスト)といいます。今回では、”予約”というコンテキスト内で意味を持つ患者や施術師などというモデルです。たとえば、患者には、属性が患者番号と名前だけです。予約にはそれで十分だからです。
たとえば、訪問するマッサージサービスを始めた場合、このモデルで役に立つでしょうか?立ちません。住所や連絡先が必要になるでしょう。しかしこの場合は訪問のための要求であり、”予約”のモデルとは違う意味になりそうです。
一つの手として、”訪問”というコンテキストがあるということを意識した方がよいかもしれません。そうなった場合は”訪問”コンテキスト内の患者モデルは違う目的のための存在することになるでしょう。たまたま、”患者”という記号が同じだけであって、その記号を支えている文脈によって意味が異なることを示しているわけです。分ける利点としては、”予約”システムを開発しているチームと、”訪問”システムを開発しているチーム、が使っているユビキタス言語が分離されているので混乱がおきにくいということでしょう。とはいえ、”予約”と”訪問”間で患者の紐付けを行うことがあります。その場合はモデルの変換によって統合を行います。この変換ルールはコンテキストマップと呼ばれます。*5 横道に逸れましたが、今回の場合は、クライアントもサーバも”予約”という同一のコンテキストに存在すると考えてよいです。基本同じ図で考えてよいと思います。じゃぁ実際どのようなアーキテクチャを採用するのかという話題は様々あると思いますが、個人的な考え方を次のエントリにまとめますね。
http://j5ik2o.me/blog/2013/05/11/sinario-model-code/#comment-893794195 ちなみに細かい点ですが、今回の図のReservationとReservationRequestの関係が汎化になってますが、単純な1:1関連かな?と思いました。
モデルとしては別ものなので汎化する必要はなさそうですね!
*1:目的以外の表現は不要ですね。予約には名前しか必要ないのに住所や生年月日までモデルに含めようとすることは無駄です。何でもかんでもドメインモデルに詰め込むとファットなモデルになるので注意してください。
*2:IntelliJのプラグインをさくっと入れればすぐに書けます。この手の図は議論が終わったら捨てるようにしてます。保存しててもいいけど変わる前提で。だからマークアップでさくっと書けるのがよいですね。図を書くのに時間を書ける必要はまったくないと思います。
*3:特に、可変なエンティティは基本的には共有しない方がよいので、代わりにエンティティの識別子を共有するとよいでしょう。
*4:シナリオにあがった言葉からドメインモデルを抽出し、それをコードにマッピングするわけですが、コードに落とす時に日本から英語になるのでどうしても概念の翻訳が発生してしまう可能性がある...。まぁ、日本語と英語の対応表を作るとか簡単に思いつきますが、メンテされなくなるので、コードのコメントに書くとよいかもしれない。
*5:もう一つ共有カーネルというパターンもあります。”予約”と”訪問”でモデルを共有しようというパターンです。つまり、患者は予約と訪問のコンテキストを跨ぐことになります。一方が一方にリファクタリングなどによって設計上の概念に影響を与える可能性があるため、コンテキストマップより慎重さが求められるでしょう。