DCI on Java について考えました
先日アレグザンダー祭りに行って色々感銘を受けたのですが、いまさら感想を書くよりはコードを書いた方が良いよねと思いました*1。以下はDCIアーキテクチャをJavaで実装したメモです。まだ聞きかじりなので間違いもあるかも。
といっても、元ネタはあるので、写経に近いですね。
- http://www.markn.org/blog/2009/06/dci-on-scala.html (Scalaによる実装)
- http://www.jroller.com/sebastianKuebeck/entry/object_oriented_programming_2_0 (←こちらの例は余りに無駄な実装が多すぎる。。)
概要
DCIの例題によく出てくる銀行の送金処理を考えて見ます。この例って実は全然リアルじゃないんだけど、まあわかりやすいですよね。。
結論からいうと、Taitsもmixinも無いJavaでDCIを実装する場合は、RoleObjectパターンを使うのがもっともわかりやすいです。
大雑把に分けると
- コンテキスト(DCIのC)
- ユースケースに特化したクラス。コントローラとかコマンドに近い感じ。
- 相互作用(DCIのI。Interactions)
- 相互作用をあらわすクラス。これをRoleオブジェクトで表現する。
- データ(DCIのD)
- ドメインの情報を扱うクラス
すんごく大雑把に言うと、コンテキストがロールを通してデータを扱うような感じにすれば良い感じ。RoleObjectパターンの変種で実装してみます。
コンテキスト
Usecaseなど特定コンテキストを表現するオブジェクトです。送金処理を実装しています。
class Context {} class TransferUsecase extends Context { public void execute (String a, String b, int money) { Account accountA = find(a); Account accountB = find(b); //送金用のロールに割り当てる //ドメインオブジェクト.role.asでRoleを取得 // Role.onでコンテキストを割り当てる SourceAccount source = accountA.role.as(SourceAccount.class).on(this); TargetAccount target = accountB.role.as(TargetAccount.class).on(this); source.transferTo(target,money); //画面表示用のロールに割り当てる AccountView fview = accountA.role.as(AccountView.class).on(this); AccountView tview = accountB.role.as(AccountView.class).on(this); fview.show(); tview.show(); } /** コンテキスト固有の画面表示ロジック*/ public void show(String str) { System.out.println(str);} /** DBからの検索(のエミュレート)*/ public Account find(String accountNumber) { return new Account(); //ダミーです } }
Contextを継承したTransferUsecaseにおいては、まずはAccountのデータクラスを特定します。この部分はどこで実装すればいいか謎なんですが、とりあえずContextにやらせています。
さらに.role.asというメソッドを呼び出して、送金元口座(SourceAccount)、送金先口座(TargetAccount)というロールを「被せて」います。ちなみにonというメソッドは俺オリジナルです。どのコンテキストで実行しているかRoleに教えています。
その後は、source.transferTo(target,money); によってビジネスロジックの実行を行っています。
最後にAccountオブジェクトにAccountViewというRoleを被せて画面表示しています。このユースケースに関するロジックはすべてこのコンテキストから一望できる感じなのですごく分かりやすいですね。
データ
Accountの実装は非常に簡単です。俺の例では振る舞いを完全に抜いてます。
class Account { public transient final Roles<Account> role = new Roles<Account>(this); public int balance = 10000; }
blanceは残高。roleはRoleObjectパターンを間単に利用可能にするためのギミックです。
Role
Roleは特定の型をラップするインタフェース。applyによってラップ対象のクラスを設定し、onによってコンテキストを設定します。パラメタライズドなのでどんなクラスでもOKな感じです。
interface Role<T> { public Role<T> apply(T t); public Role<T> on(Context contxt); }
Roles
class Roles<T> { T target; public Roles(T target) { this.target = target;} public <R extends Role<T>> R as(Class<R> clazz) { try { Role<T> r = clazz.newInstance().apply(target); return clazz.cast(r); } catch(Exception e) { throw new Error(e); } } }
Rolesは汎用的なRoleファクトリです。細かい説明は省く。データオブジェクトにactas(Role)みたいなメソッドを実装するよりは楽かなーと思って導入しました。
Roleの実装
送金元口座
class SourceAccount implements Role<Account> { Account account; Context context; public void transferTo(TargetAccount target , int amount) { withdraw(amount); target.deposit(amount); } public void withdraw(int amount) { account.balance -= amount;} public SourceAccount apply(Account _account) { this.account = _account; return this; } public SourceAccount on(Context context){ this.context = context; return this; } }
送金先口座
class TargetAccount implements Role<Account> { Account account; Context context; public void deposit(int amount) { account.balance += amount; } public TargetAccount apply(Account _account) { this.account = _account; return this; } public TargetAccount on(Context context){ this.context = context; return this; } }
ここらへんはコードを読んだ方が早いですね。Roleインタフェースを実装するためのメソッド(ちょっとめんどくさい)とビジネスメソッドのtranferToとかdepositとかwithdrawがそれぞれのRoleに実装されています。
画面表示
class AccountView implements Role<Account> { Account account; Context context; /** Roleからのコンテキストの利用例 */ public void show() { TransferUsecase.class.cast(context).show(account + "->" + account.balance); } public AccountView apply(Account _account) { this.account = _account; return this; } public AccountView on(Context context){ this.context = context; return this; } }
画面表示用のロールはちょっと特別です。contextを呼び出して自分の情報を出力しています。このようにRoleはかなりリッチな振る舞いが出来るのがメリットですね。DBへのアクセスや他システム通信を実装していても、関心事単位に分割されているので必要以上に実装が複雑になることはありません。
呼び出し
コンテキストの実行は普通のコマンドオブジェクトっぽくやってみます。
public class Main { public static void main(String[] args) { new TransferUsecase().execute("1111","2222",3000); } }
ちなみにMain.javaというファイルに紹介したコード全部貼り付ければ動きます。
感想
RoleObjectパターンを使った実装はTraits使うのに比べて下準備は必要ですが、使用感はかなり快適です。オブジェクト生成後にRoleをアタッチできるのでTraitsよりも優れている面もあります。
特定のコンテキストに特化したRoleObjectを作らずとも、コンテキストにtransfer(Account from,Account to , int money)みたいなメソッド作ればいいじゃんという反応は当然予想できるし、有効な反論も少ないかも。。
ただ実装面からメリットとして以下のようなものがあります。
- 一部のRoleをMockにしたりできるのでテスタビリティが高い
- Roleオブジェクトには関係あるメソッドが並ぶので凝集度が高い
- データモデルには存在しないが、ユーザのマインドモデルには存在するオブジェクトを導入しやすい
- そもそもAccountすらもRoleな気もします。口座の情報はもっと別の形で保存されてそう。(コプリエンもそんな事をいってた気がします)
- オブジェクト指向のメタファをフルに利用できる(これを喜ぶのはOO厨だけ??)
もちろんDCIアーキテクチャはプロダクトとプロセスの両輪から見る必要があると思います。そのような視点ではより強い必然性があるでしょう。
個人的にはRoleを適切にモデリングするのが難しそうだなーと考えています。実際に採用するときの壁はここにある。。
*1:多少の逃避行動でもあるんですが。。