より良いトランザクションスクリプトを目指す
ファウラーのエンタープライズアプリケーションアーキテクチャパターン(PofEAA)において、 ビジネスロジックのアーキテクチャにはドメインモデルかトランザクションスクリプトかという二択があります。
仮にプレイヤーの名前変更(ゲームでは可能なことも普通にあるので)をするとします。
- ドメインモデル
var user = _repository.Find(userId);
user.ChangeName(name); //バリデーションは中で行われる=ビジネスロジックがオブジェクトにある
_repository.Save(user);
- ドメインモデル貧血症
//ロジックがモデルオブジェクトの外にある if(!IsValidName(name)) { throw new ArgumentException("name"); } var user = _repository.Find(userId); user.Name = name; _repository.Save(user);
if(!IsValidName(name)) { throw new ArgumentException("name"); } _dao.UpdateName(userId, name);
daoはData Access Object です。PofEAAでは「テーブルデータゲートウェイ」という名前で紹介されている、SQL文と実行を閉じ込めただけの簡素なデータアクセスクラスです。
SIにいた人なら「ダオ」のほうが通りが良いのではと思います。テーブルデータゲートウェイなんて現場で聞いたことが無い(私がそうというだけかもしれませんが)。
それはさておき、アプリで使われるSQLを完全に制御したい(手書きSQLを使いたい)なら基本的にデータアクセスはO/RマッパーやActiveRecordではなくDAOを選択することになります。
そしてデータアクセスでO/RマッパーやActiveRecordを使わないということはドメインモデルを選択できないということでもあります。
ドメインモデルに実装されるビジネスロジックは、最終的に集約のルートを頂点としたプレーンなオブジェクトグラフを更新するだけで、データの保存に関しては関知しません。O/Rマッパーが、オブジェクトグラフをどう保存するかを知っていてよしなに保存するのです。
DAOが、単に変更後のオブジェクトグラフを渡されてこれを正しく保存せよと言われると大変厳しい。
単に一つのエンティティを保存するだけなら、力業で全プロパティをUpdateする汎用UPDATE文をDAOに用意すれば効率はさておき保存は可能です。しかしオブジェクトがコレクションを持っていたりするともうだいぶ厳しい。
class Blog { ... //これへのAddをDAOで検知するの厳しい public ICollection<Comment> Comments { get; } }
これをスマートに検知できるのはもうシンプルなSQLのラッパーの範疇を超えています。
力技ならCommentDaoに全行InsertOrUpdateみたいなことをできるかもしれないけど、これをやるともうSQLを完全に制御して性能を稼ぐDAOのメリットがありません。O/Rマッパーよりはるかにひどい性能劣化を引き起こしてしまいます。
つまり、SQLを細かく制御したいならばドメインモデルを選択することはできず、トランザクションスクリプト一択だと思います。
なおSQLを細かく制御するということは、「データがどのように保存されるべきか」をビジネス層が知っているということです。少なくとも完全な隠蔽はできません。だからビジネスロジックであるにも関わらず「トランザクションスクリプト」という名前が付けられているのでしょう。
より良いトランザクションスクリプトを目指す
トランザクションスクリプトはアンチパターンではありません。PofEAAでファウラーが「ビジネスロジックが複雑になってきたらオブジェクト指向的な手法のほうが良い」と連呼しているし DDDも有名なのでトランザクションスクリプトはなんとなく肩身が狭いですが、RDBが性能のボトルネックになりうる環境でSQLを細かく制御したいというのは自然な欲求であると自分は思います。 DAOは実装がシンプルなのでオーバーヘッドも少なく、バッチでの使用もまったく問題ありません。O/Rマッパーをバッチで使えと言われたらやったことはないですが個人的にはやや不安だったりします。チェンジトラッカーさん耐えれられるのかな…
というわけで、より良いトランザクションスクリプトのためにモヤモヤと考えていること。
リポジトリパターン
トランザクションスクリプト+DAOではリポジトリパターンは不要と考えます。集約の単位で不変条件を守りながらデータの出し入れをするDDDのデータアクセスレイヤーがリポジトリです。DAOことテーブルデータゲートウェイはテーブル単位なのでリポジトリより細かい単位なので、リポジトリでDAOをラップすれば集約の単位での保存が可能になりますが、おそらくラップしているリポジトリ側で組み合わせ爆発的にメソッドを生やさないといけなくなってくるので、DAOをバラバラに触ったほうが効率が良いでしょう。また、単一エンティティの集約の場合は、リポジトリは単にDAOに引数をリレーするだけの存在になってしまいます。これを維持するのはつらかろう…ということで不要として良いのではと思います。
もしDAOに相当するものを流行りに流されてリポジトリと呼んでいるなら改名したほうが良いでしょう。パターンは同じものを指してこそ。誤用は避けていきたい。
「ふん!リポジトリっていうのかい?贅沢な名前だねぇ…!今からお前の名はダオだ!いいかい、ダオだよ!」
テスタビリティ
I/Oのレイヤーを差し替えてテストしやすくする場合、トランザクションスクリプトが直接DAOを触るので、DAOにinterfaceを持たせて差し替え可能にすることになりますので、DAOのファクトリをDIで挿せばテスタビリティは問題なし。
余談ですが、手書きSQLを使うパターンには行データゲートウェイもあります。これはデータのレコードが自らのCUDを行うオブジェクトです。
public class HogeRecordGateway { //このあたりはDBのカラム public long Id { get; set; } public string Name {get; set; } ... //ここからはCRUDのCUD public void Insert() { ... } public void Update() { ... } public void Delete() { ... } }
テスタビリティを考えると結局はInsert/Update/DeleteはDIされたDAO(テーブルデータゲートウェイ)に任せる必要があるため、行データゲートウェイを生成するファクトリまで必要になってしまうのでイマイチだと思います。
PlainなデータクラスとDAOを使うほうがマシではないかと。
ドメインサービス
DDDにおける「エンティティでもバリューオブジェクトでもない、関数のようにしかモデリングできないもの」がサービスになりますが、これはトランザクションスクリプトでも問題なく使用できます。というかエンティティ側にロジックを持たない以上は「ドメインサービスの組み合わせ+流れるデータ+DAOでの保存=トランザクションスクリプト」という形になってくるはずで、比重が増えます。
CQRS
問題なく使用できますが、コマンド側にもドメインモデルが登場しなくなるのでクエリがドメインモデルをバイパスできるメリットは感じにくくなるかもしれません。キャッシュ参照をクエリ側に閉じ込めることで見通しを良くするメリットは依然として享受できるはずです。(キャッシュ消しはコマンド側からも行う必要があるので、完全に手が切れるわけではありません)