ソフトウェアの分析・設計で、
◎責任駆動設計 ( RDD : Responsibility-Driven Design )
◎レイヤ構造 ( Layered Architecture )
の二つは、ソフトウェアをわかりやすく扱いやすくするための、基本かつ強力なテクニック。
Domain-Driven Design (DDD)の、16章「構造設計」で、この二つを駆使した、Responsibility Layers パターンが登場する。
責任駆動設計
オブジェクトごとに、単純で、明確な役割を持たせるように、設計すること。
たとえば、(日時ではなく)日付だけを 扱う BusinessDate クラスや、DateRange クラス。
日付や期間を扱う時は、こいつらに全部まかせる。
時・分・秒とかは、別の TimePoint クラスとか、Interval クラスとか、Duration クラスとかが、担当する。
「誕生日と年齢」とかは、BusinessDate クラスを内部の保持する、DateOfBirth クラスにまかせる。
郵便番号は、String クラスではなく、PostalCode クラス、電話番号も、Telephone クラスにまかせ、氏名は、PersonName クラス。
こいつらを、顧客番号で識別する、Customer オブジェクトが Aggregate のルートになって束ねる。
Customer オブジェクトは、Entities パターン、それ以外は、Value Objects パターンで、設計・実装。
...
こんな感じで、オブジェクトに単純で、明確な役割を持たすように設計していくのが、責任駆動設計。
顧客オブジェクト自身が、String,Date,int で、20も30もフィールドを持つ設計・実装も可能。
でも、これは、顧客オブジェクトの責任範囲が広すぎる。責任範囲を狭めてあげないと、パンクしちゃいますね。
役割が単純で明確なオブジェクトに分割する、責任駆動設計が当たり前になってくると、ソフトウェアは、格段に、わかりやすく、また、変更も、局所的になって、安心してできるようになる。
レイヤ構造
責任駆動設計だと、オブジェクトの数は、増えてきます。
ひとつひとつのオブジェクトの責任範囲を小さくわけ、分担するわけだから、どうしても、クラスの種類は増える。
当然、パッケージ( Modules パターン)を使ってグルーピングする。
でも、このまま放置すると、パッケージ自体の整理が難しくなってくる。
あるパッケージと別のパッケージが、双方向に、参照(依存)する関係がはじまると、コードは、スパゲッティになり、ぐしゃぐしゃになってくる。
どのパッケージが、誰から参照されているか、だれも、正確にはわからない。
ツールを使って、依存関係をレポートすると、レポート量が多く、内容も複雑で、読む気もしない。
構造設計を軽視すると、オブジェクトやパッケージの数が増えるたびに、破綻のカウントダウンが進行する。
レイヤ構造は、「構造設計」の基本中の基本。
Upper Layer : 上のレイヤは、下のレイヤを知っている(依存する)
Lower Layer : 下のレイヤは、上のレイヤを知らない(依存しない)
パッケージを、それぞれのレイヤに配置して、このレイヤ構造の参照制約を徹底すれば、構造がすっきりとし、安全になる。
参照を制約する
下のレイヤから、上のレイヤのオブジェクト(パッケージ)への参照を禁止するだけで、コードはスパゲッティではなくなる。
スパゲッティ構造から、ラザニア構造になるわけだ。
参照を制約しなければ、どのクラスは、すべての Public クラスの、Public なメソッドやフィールドを、どこから参照してよい、ことになる。なんのことはない、悪名高いグローバル変数の乱用を、オブジェクト指向っぽくやっているだけ。
グローバルの自由な参照こそ、スパゲッティコードが生まれる原因なんだ。
参照の方向や範囲を制限すれば、それだけ、スパゲティコードが生まれにくくなる。
理屈は単純。
実践は、最初は、たいへんかもしれない。
発想の転換とか、チームや個人の価値観や規律とかの問題。技術的な問題ではない。
ソフトウェアがちゃんと動いていても、構造が、スパゲッティでは、ダメだ、という思いを共有し、かつ、そうしないように、日々努力することを自然にできるチームか、どうか。
構造設計が貧弱、劣悪でも、ソフトウェアは動く。動いているなら、それは正しい、という人・チームが多い。多いということは、それこそが、真実なのかなあ...。
ドメインモデルとか、DDD とかの設計の考え方にこだわる(?)のも、少数派であることはまちがいなさそうだしなあ。
まあ、自分は、現場で、ドメインモデルやDDDに取り組んでからのほうが、良い仕事が楽にできるようになったと思っているので、この方向で、もっと腕を磨きたいと思っている。
ドメイン層内部のレイヤ構造の基本パターン
Responsibility Layers パターンに、登場する、5つのレイヤは、ドメインオブジェクトの整理、ドメイン層内部の構造設計の参考になる。
◎ Decision Support 層 ( 意思決定の支援 )
◎ Policy 層 ( ビジネスの決め事 )
◎ Commitment 層 ( 約束ごと )
◎ Operation 層 ( 日々の業務のアクションとその結果 )
◎ Potential 層 ( 業務実施の制約条件 )
という感じで、ドメインオブジェクトを整理すると、わかりやすいよ、ということ。
モデリングとかで、このレイヤ構造を意識すると、ドメインオブジェクトの粒度とか、役割の分担方法とかに、いろいろなヒントを提供してくれる。
実際には、厳密なレイヤ構造(厳密な参照方向の制限)にならないところもあるけど、それは、リファクタリングネタというか、課題としていろいろやってみている。
なお、Business Date とか、Money とかの プリミティブな Value Object は、さらに、この下のレイヤに集めるようにしている。
下から、説明したほうが、わかりやすいかなあ。
Potential 層のオブジェクト
業務を実行するときに、構造的に、静的な制約条件がたくさんある。
DDD のサンプルで使われている、貨物の輸送状況トラッキングだと、Customer(顧客)オブジェクトは、この構造的な制約になる。
Customer オブジェクトは、別のアプリケーションで管理され、識別番号も、そっちで発行・管理されている。
貨物を追跡する時に、「どの顧客」の貨物であるかは、追跡アプリは、外部から与えられた「顧客のリスト」に限定しないといけない。
マスターに登録された顧客(番号)だけ、という制限があるわけだ。
追跡すべき輸送イベントの種類 ( EventType )とか、イベントの結果、変わる貨物の状態の区分 ( StatusType ) とか、Enum 型で表現する、ドメインオブジェクトも、Potential 層の住人。
貨物の追跡アプリで、参照できる地名も、国連のさだめた UN LOCODE というコード体系でのみ、表現している。
UN LOCODE が、この貨物追跡ドメインにおいては、制約になっている。
Potential 層というのは、このように、外部で発行・管理している識別コード体系、とりうる値が有限で固定の、種類(コード)や、区分(コード)でを、知っているオブジェクトを集める場所、ということ。
もちろん、これらのオブジェクトは、上の層は、全く知らない。
(参照しない)
Operation 層のオブジェクト
日々の業務で起きていることを表現するためのオブジェクト。
アプリケーションの中心は、だいたいがこの層のオブジェクト。
基本は、三つ
・Event オブジェクト
・関心事オブジェクト
・State オブジェクト
Event オブジェクトは、貨物を受け取った、貨物を積み込んだ、船が出発した、船が到着した、貨物をおろした、....というように、貨物に発生した事実を、通知・記録するためのオブジェクト。
在庫管理であれば、入庫イベントや出庫イベント、預金通帳なら、入金イベントや出金イベント、受注管理であれば、注文受領イベント、出荷指示イベント、出荷通知イベント、...。
ビジネスのさまざまな活動や起きた事象の中で、「関心の対象」となる、起きた事象を、
・イベントの対象
・日時
・起きたイベントの種類
・その他の補足情報
として、表現するのが、イベントオブジェクト。
DDD サンプルだと、イベントの対象は、「貨物」、イベントの種類は、さっき書いた、「受け取った」「積んだ」「降ろした」「渡した」などの事象。
補足情報としては、「場所」を UN LOCODE で表現。
関心事オブジェクト ( OoC : Object Of Concern ) は、この場合は、もちろん「貨物」。
貨物の輸送状況を知るのが、このアプリケーションの中核の価値。
関心事オブジェクトを見つけることは、普通はとても簡単。
業務的に、
・検索したいオブジェクト
・一覧したいオブジェクト
・詳細を表示したいオブジェクト
・アクションを起こしたいオブジェクト
を特定する。
一覧画面とか、詳細画面とそこに登場する「ボタン」を見れば、関心事が何か、一目瞭然ですよね。
で、関心事オブジェクトは、Entity であり、Aggregate のルートです。
そして、その関心事オブジェクトが、集約しているのが、「状況」を表現した、STATE オブジェクトというわけです。
貨物オブジェクトの現在の「場所」を表現しているのが、STATE オブジェクト。 DDD サンプルだと、Delivery オブジェクトが、現在の貨物の場所を知っているオブジェクト。
業務アプリケーションの基本は、
・イベントオブジェクトを受信し、
・関心事オブジェクトの STATE オブジェクトを適切に更新し、
・イベントが起きたことを記録し、
・イベントが起きたことを通知する
のが基本パターン。
Operation 層は、この基本パターンを表現するための、 Event, OoC, State のオブジェクトを集める場所。
Operation 層のオブジェクトは、下位のレイヤ Potential 層で定義された、コードマスター、種類マスタ、区分マスターなどを参照し、また、そこで定義されたものだけに制約される。
わりと単純なアプリケーションなら、この Operation 層 over Potential 層の二層構造で、実現できちゃうことも多い。
Commitment 層
もうちょっと役立つアプリケーションにするなら、Commitment オブジェクト ( 約束を表すオブジェクト)を登場させる。
DDD サンプルだと、貨物の宛先(=顧客との配送の約束)Specification、輸送のスケジュール(業者内部の予定)Itenary などが、この Commitment オブジェクトにあたる。
ビジネスでは、約束(コミットメント)して、それを、確実に、実行することが基本中の基本。
開発だって、納期を約束して、なにがなんでも、それをまもんなきゃいけない。
朝会の約束したら、8時には、会議室に集まらないといけない。
ビジネスのアプリケーションは、簡単に言えば、
・約束(Commitment ) を記録し
・約束を実行しつつあるイベント(アクション)を記録し、
・それを、現在の状態 ( State ) に反映し、
・約束(予定)との差異をチェックし、
・問題があれば、対応アクションを起こす
ための道具が必要。
Commitment オブジェクトは、もっとも重要な関心事オブジェクト ( OoC ) だというわけだ。
DDD サンプルでは、貨物オブジェクトが、関心事オブジェクトで、その内部で、Commitment (約束・予定)と State ( 現在 ) の状態を持つ設計になっている。
Commitment オブジェクトは、Operation 層の住人(という性格が強い)
在庫管理でいえば、Commitment 層を導入すると、入庫予定、出庫予定、将来の引き当て予約、...というように、アプリケーションの守備範囲が広がる。ここまでサポートしてくれたほうが、もちろん役立つソフトウェアになる。
預金通帳なら、過去の入金・出金に加えて、入金の予定と出金の予定まで管理すれば、今月末の余裕資金とか、がわかるようになる。
いまどきのビジネスソフトは、こういうレベルを期待されているはず。
Commitment 層は、役に立つ、喜んでもらえるソフトウェアのキーフィーチャーといえる。
Policy 層
ビジネスの決め事がちょっとややこしくなると、独立したオブジェクトとして表現したほうが、すっきりする。
ちょっとしたビジネスルールは、Value Object や、Entity オブジェクト、Domain Event オブジェクトにまかせる方法もある。
でも、業務で、次のような言葉がでてきたら、そのまま、Policy オブジェクトにして、それを、Policy 層に集めちゃう。
・Pricing Policy
・OverBooking Policy
・Cancel Policy
・Routing Policy
...
ホテル業務などだと、Over Booking Policy と、Cancel Policy は、基本のビジネスルールですね。
輸送業界でもそうです。
Policy オブジェクトは、手続き型の表現になりがちで、あまりお薦めではないけど、業務で「○○ポリシー」という概念がでてくるなら、それは、文句なしに、Policy オブジェクトとして、素直に、実現する。
あるいは、契約書や申込書の裏に書かれたルール集、携帯電話の複雑な課金体系、審査業務マニュアルのチェック事項集、複雑な表に膨れ上がった割引価格表、...
こういう実体を使っている業務なら、こういうルールのカタマリを、素直にオブジェクトにしたほうが良い。これも、Policy オブジェクトであり、Policy 層の住人。
<注意1>
Potential 層も、Policy 層も、業務のルールや「制約」を表現する点では同じ性格。
違いは、ルール変更の頻度と柔軟性。
Potential 層は、このドメインでは、変更は不可です。静的で構造的な制約。
おいそれと変更はできないものを、ここに置く。
Policy 層のルールは、ビジネスのたんなる(?)決め事であり、場合によっては、特例として、ルール違反でも、OKとする、というような業務運営(Operation) も十分にある。
だから、Policy 層のオブジェクトの実装や、Policy オブジェクトを使ったのルールの強制は、ほどほどに設計する必要がある。
業務上、レアであるが、必ず発生する、特例ケースを、どう扱うかが、難易度は高いけど、業務アプリの設計者の腕のみせどころ、でもある。
<注意2>
頻繁に変える可能性がある区分コードなら、それは、Policy 層に置く。
「○○区分」とかが登場したら、単純に、Potential 層で、Enum で実装、とか、やっちゃいけない。
もしかしたら、今年の夏のボーナスの販売キャンペーンでだけ、使いたい区分かもしれない。
これは、名前は、「区分」でも、静的・構造的な制約でなく、動的・振舞の制約。
ビジネスのルールや区分を、Policy とするか、Potential とするかは、けっこう重要な設計判断。
まあ、やりながら、リファクタリングしていく、というパターンもありますね。
Policy 層は、最初から、「ルールの変更あり」という前提で、作るべし。
Decision Support層
利用者に判断を求めるために、参考情報を提供するためのオブジェクト群の層。
特定の貨物の現在位置は、Operaiton 層のオブジェクトだけで、知ることができる。
貨物全体の流れ、顧客との約束の達成度、ご配送や再配送の発生頻度、...
こういう情報を、わかりやすく集約するためには、分析手法とか、集計方法の知識が必要。
こういう知識を持つのが、Decision Support 層のオブジェクトの役割。
リコメンド機能とかを実現するオブジェクトも、Decision Support 層の住人ですね。
アプリケーション間の関係
Decision Support 層は、そもそも別のアプリケーションに分離して、Operation 系のアプリケーションとは、 Customer - Supplier の関係になることが多い。
また、あるアプリケーションの Potential 層が、他のアプリケーションでは、Operation 層になるのが普通。
DDD サンプルだと、Customer オブジェクトは、Potential 層だけど、隣接して、顧客管理アプリケーションが存在して、そこから、Customer 情報を受け取ることを想定している。
エンタープライズアプリケーションというのは、こうやって、社内それから社外まで含めてさまざまなアプリケーションが、Potential 層 - Operation 層 - Decision Support 層 のアプリ内の位置づけを変えながら、階層的に絡み合っている世界。
アプリケーション連携を設計するときも、
・Operation 層を、別アプリの Potential 層に
・Operation 層を、別アプリの Decision Support 層に
提供する、というパターンが多い。
また、水平連携ということで、
・イベントの発生元の Operation 層が、別のアプリの Operation 層に、それを通知
というのも、非常によく出て来るパターン。
メッセージングが面白い
ここらへんのつなぎを、最近は、
・Mule ESB
・REST スタイルの API
・EDA ( Event-Driven Architecture )
・SEDA ( Staged Event-Driven Architecture )
・EIP ( Enterprise Integration Pattern )
・Domain Event と Event Sourcing パターン
...
とかと格闘(遊び?)しながら、実践でやりはじめたところ。
一言でいうと、DB中心のアーキテクチャから、メッセージング中心のアーキテクチャに発想がシフト中、という感じ。
もちろん、業務アプリで、DB(永続化)の重要性はかわらない。メッセージングが、永続化と並ぶ、二本目の柱として急成長中、というイメージですね。
DB(永続化)+メッセージングは、「記録」と「通知」という業務ドメインの本質を、ITで素直に実現する、単純でわかりやすい構図だと思っている。
なぜ、DBか? なぜ、メッセージングか?
それは、ビジネスでは、「記録」と「通知」が基本だから。