DCIアーキテクチャの概要を整理した上で、DDDに登場するローンシンジケートを用いたサンプル実装を示す。
DCIアーキテクチャの概要
Trygve Reenskaug氏とJames O. Coplien氏によるDCIアーキテクチャの構想は、「DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien」にて解説されています。ここでは、オブジェクト指向の本質が人間のメンタルモデルを捉えることにあるとした上で、オブジェクト指向の問題点とその解決方法が語られます。オブジェクト指向の問題とされているのは、構造を捉えることに長けている反面、ふるまいをとらえることが苦手であるという点です。具体的には、特定のふるまいをどのクラスにおくべきか悩んだり、エンティティクラスが大量のメソッドで肥大化してしまうといったことが挙げられるでしょう。
この問題に対する解決は、オブジェクトが持つメソッドの性質を2つに分けることから始められます。
残高を減らすことも、引き出しをすることもできるという事実は、メソッドとしてまとめられる。これはどちらもふるまいである。しかし、これら2つのふるまいは劇的に異なるものだ。残高を減らすということは単にデータの特質にすぎない。つまり、データが何かということだ。引き出しをするということはデータの目的を反映している。つまり、データが何をするかということだ。引き出しを制御するということが暗示しているのは、トランザクションの動作、ユーザとのインタラクション、リカバリ、エラー条件の処理、業務ルールなどだが、これらはどう考えたとしてもデータモデルの考え方をはるかに凌駕している。
DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien
このうち、後者の「データが何をするのか」を捉えるために導入されるのが「ロール」の概念です。データモデルが捉えられないふるまいをロールを通じたインタラクションとして捉えようとするのです。このロールは「一連の操作を実行する上での自然な境界を提供する」ことができるため、特定のデータモデルが肥大することも、逆に細分化され過ぎることもなくなります。
「データ/構造」と「ロール/ふるまい」は特定のオブジェクト上で重なり合う(例えば、「私の預金口座(データ)」が「振り込み元(ロール)」になる)ことになりますが、この紐づけを行うのが「コンテキスト」です。
例えば、預金口座が振替ユースケースの振り込み元口座というロールにおいてなんらかの責任を果たすということを、エンドユーザは理解している。このこと、つまりロールの見方とデータの見方の間にある紐づけも、ユーザが持つ認識モデルの一部なのだ。われわれはこれをユースケースシナリオを実行する上でのコンテキストと呼ぶ。
DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien
以上より、DCIアーキテクチャの基本的な構成要素が導きだされます。
データ
ユーザのメンタルモデルにおける構造を表象する概念
ロール
ユーザのメンタルモデルにおけるインタラクションを表象するのに用いられる概念
コンテキスト
各データに対し、特定のインタラクションにおけるロールを与えるもの
コンテキストがやらなければいけないのは、ロールの識別子を適切なオブジェクトに結びつけることだけだ。そうすれば、やらなけらばいけないことは、そのコンテキストにとって「エントリ」となるロール上にあるトリガメソッドをキックすることだけで、それによってコードが実行される。
DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien
サンプル:ローンシンジケート
ここまで解説したDCIアーキテクチャについて、具体的にサンプルを見て行きます。題材として用いるのは、Eric EvansのDDDで紹介されている、ローンシンジケートのモデルです(Chapter 8, Chapter 10)。サンプルコードは、こちらのGoogle Codeにて公開しています。
要件
ここでいうローンシンジケートとは、1つの会社ではまかないきれないほどの大金を貸し出すために結成される組織です。ローンシンケゲートは結成時に、ローン上限金額と、参加する会社のローン出資の割合が定められます。実際の引出が行われた場合には、あらかじめ定められた割合に応じて、各会社が金額を出し合います。借り手は上限金額までならば、何度でも引き出すことが可能ですし、途中で任意の金額を返すこともできます。また、ローン出資の割合は固定されたものではなく、1度目の引出しの後、A社がB社に10%分譲る、ということもあり得ます。したがって、返却時における各会社への割当は、ローン出資の割合ではなく、貸し出している金額の割合となります。(アーキテクチャの実現が目的であるため、端数や利子などの細かい仕様は実装していません。)
以下、テストコードの一部を示します。(数字の後ろに付いているGはGroovyでBigIntegerへのキャストを意味します。)
Company a = new Company(id:10, name:"A") Company b = new Company(id:20, name:"B") Company c = new Company(id:30, name:"C") LoanSyndicate syndicate = new LoanSyndicate().buildFacility(1) syndicate.joinFacility a, 50G // A社が50% syndicate.joinFacility b, 30G // B社が30% syndicate.joinFacility c, 20G // C社が20% syndicate.facilityLimit 100000G // 上限10万 syndicate.draw 10000G syndicate.pay 5000G assertEquals 2500G, syndicate.facility.loan.shares[10].amount assertEquals 1500G, syndicate.facility.loan.shares[20].amount assertEquals 1000G, syndicate.facility.loan.shares[30].amount assertEquals 95000G, syndicate.facility.limit
データモデル
基本的なデータモデルを構成するのは、以下の3クラスです。
public class Facility { int facilityId BigInteger limit = 0 Loan loan = new Loan() SharePie shares = new SharePie() // ローン出資割合 } public class Loan { int loanId SharePie shares = new SharePie() // 実際に貸し出し中の金額割合 } public class Share { Company owner BigInteger amount }
なお、Facility
とLoan
が持つSharePie
の実体は、Company
のidをキー、Share
を値としたマップです。
ふるまいを持つエンティティ
エンティティにふるまいを持たせた場合、Facility
クラスは以下のようになります。
public class Facility { int facilityId BigInteger limit = 0 Loan loan = new Loan() SharePie shares = new SharePie() // PercentagePieRole boolean validate() { boolean allPositive = true shares.each { allPositive = allPositive || 0 <= it.value.amount } allPositive && 100G == shares.totalAmount() } // PercentagePieRole void transfer(Company from, Company to, BigInteger transferAmount) { Share fromShare = shares.get(from.id) Share toShare = shares.get(to.id) if(fromShare.amount <= transferAmount) { throw new IllegalStateException("not having enough amount") } fromShare.amount -= transferAmount toShare.amount += transferAmount } // PercentagePieRole BigInteger allot(int ownerId, BigInteger whole) { (whole * shares[ownerId].amount / 100).toBigInteger() } // LenderRole void addMember(Company company, BigInteger amount) { shares.put company.id, new Share(owner:company, amount:amount) loan.addMember company } // LenderRole void draw(BigInteger amount) { shares.each { int ownerId = it.value.owner.id loan.increase(ownerId, allot(ownerId, amount)) } limit -= amount } // LenderRole void pay(BigInteger amount) { loan.decrease(amount) limit += amount } }
Facility
が持つローン出資割合の制御ロジックと、Loan
を含めた集約ルートとしての引出、返済ロジックが両方含まれ、エンティティが肥大してしまっているのが分かると思います。これがロールを導入することですっきりしたものになります。
DCIアーキテクチャの導入
「ロールとデータをコンテキストにおいて動的に紐づけること」がDCIアーキテクチャにおける実装上の課題の1つとなります。サンプルはGroovyで実装しているので、Mixinの機能を用いて実現します。コンテキストクラスにおいて、組合が結成される際に各データに対してロールを紐づけます。
public class LoanSyndicate { Facility facility def lender def percentagePie def amountPie LoanSyndicate buildFacility(int facilityId) { this.facility = new Facility(facilityId:facilityId) this.lender = assign(facility, Lender) this.percentagePie = assign(facility, PercentagePie) this.amountPie = assign(facility.loan, AmountPie) lender.data = facility this }
ここで、assign
メソッドがMixinを行うスタティックメソッドです。「データとロールとの紐づけ」というコンセプトを明確に表現するために作成したものです。
static Object assign(assignee, Class<?> role) { assignee.metaClass.mixin role assignee }
「貸手(lender)」も「ローン出資割合(percentagePie)」も実体はFacility
ですが、それぞれ別々のロールとして扱っています。コンテキストではロールのトリガメソッドをキックします。drawメソッドを例にコンテキストの実装を示します。
void draw(BigInteger amount) { lender.draw(amount, percentagePie, amountPie) }
drawメソッドはMixinされるLender
クラスに実装されています。
void draw(BigInteger amount, percentagePie, amountPie) { drawFromPieHolders(amount, percentagePie, amountPie) decreaseLimit(amount) } private void drawFromPieHolders(BigInteger amount, percentagePie, amountPie) { percentagePie.shares.each { int ownerId = it.value.owner.id BigInteger allot = percentagePie.allot(ownerId, amount) amountPie.increase(ownerId, allot) } }
Facility
クラスは純粋なデータモデルのままです。
public class Facility { int facilityId BigInteger limit = 0 Loan loan = new Loan() SharePie shares = new SharePie() }
Facility
クラスにMixinされるロールも、「貸手」と「ローン出資割合」に分けられているため、各クラスの責務は明確です。(実際の各クラスはこちらから参照して下さい。また、v1フォルダにはロールモデルを使わない実装が格納されています。)
ここで残された課題についても触れておく必要があるでしょう。完成したコードを見るとかなりすっきりしているように見えますが、実はlenderなどは型を宣言していません。これは、Mixin後もLeaderクラスにキャストすることができないことに起因します*1。最初にテストコードを書いているので、スペルミスなどがあってもすぐに直せるという見方もあるにはあるのですが、Javaをコード補完でサクサク実装することに慣れているとやや気になる点ではあります。ただし、これはあくまで生産性の問題ですので、メンタルモデルと実装を整合させることとはまた別の話と言うべきかもしれません。
もう1点、こちらはデザイン上の課題です。ロールオブジェクトとデータオブジェクトの関係を見た場合、インタラクションはロールを通じて行われるため、「ロールはデータモデルを知っているが、データモデルはロールを意識しない」構成となっています。それがどのように実装されているのか、AmountPie
の1メソッドを例に示します。
void decrease(BigInteger amount) { BigInteger totalAmount = shares.totalAmount() shares.each { Share eachShare = it.value eachShare.amount -= amount * ( eachShare.amount / totalAmount) } }
ここで、shares
という変数でアクセスしているのは、Loan
クラスのフィールドです。
public class Loan { int loanId SharePie shares = new SharePie() }
AmountPie
クラスはLoan
クラスにMixinされるため、例のようにアクセスすることができますが、例によって静的な紐づけは一切行われていません。AmountPieAssignable
のようなインタフェースをデータクラスで実装することも考えましたが、ややオーバーデザイン気味なので、今回のサンプルでは避けています。ただいずれにしても、あるロールを纏う際にデータモデルが持っているべき「能力」については、何らかの形で表現されているべきだと考えています。
まとめ
ロールという概念を導入することで、非常にすっきりとした責務の境界線を引くことができたと思います。ただし、このモデリングを行うためには従来のオブジェクト指向とは少し異なるロールベースのモデリングテクニックが必要です。実際にサンプルを実装する時にも、実は最初から完全なモデリングができていた訳ではなく、v1フォルダにあるようなオブジェクト指向ベースのクラスを一度作ってから、ふるまいをロールクラスに切り出すということを行いました。コツとしては、データモデルにふるまいを持たせている段階からコンテキストクラスはつくっておき、コンテキストクラスに対するテストコードを作成することでしょうか。そうすることで、ロールクラスの切り出しもテストをしながら安全に行うことができます。
また、データクラスからロールクラスに移した後のリファクタリングで気がついたことですが、もともと実装していた各メソッドの抽象度がそろっているほどその後のリファクタリングはスムーズにすすみます。データの操作を「HOW」ベースでべったり実装してしまうと、実は異なるロールで行うべき処理が一緒に実装されていたということになりかねません。責務が明確なアーキテクチャは、責務を明確にした実装を要求するということでもあるということです。
DCIアーキテクチャも「人間のメンタルモデルを写しとりたい」という探求の延長にあるものです。抽象度を適切に制御すること、概念を明確に区別することといった基本は、ここにおいても常に立ち返らなければならないものなのだと思います。
補足(2010/06/04)
コメントにてuehaj様に指摘して頂いた通り、「これは、Mixin後もLeaderクラスにキャストすることができないことに起因します」という一文は誤りでした。修正後のコンテキストのコードを挙げておきます。
public class LoanSyndicate { Facility facility Lender lender PercentagePie percentagePie AmountPie amountPie LoanSyndicate buildFacility(int facilityId) { this.facility = new Facility(facilityId:facilityId) this.lender = assign(facility, Lender) this.percentagePie = assign(facility, PercentagePie) this.amountPie = assign(facility.loan, AmountPie) this }
*1:Groovy歴の浅い私が単純に方法を知らないだけの可能性もあります。もし適切な方法がありましたら、ご指摘頂ければ幸いです