「Transaction Scriptはアンチパターンだ」「ドメインモデルを使うべきだ」という言説をよく耳にします。しかし、Martin Fowlerが『Patterns of Enterprise Application Architecture』で示した適用条件を読み直すと、この二項対立は単純化しすぎていることがわかります。
Fowlerが語った適用条件
PoEAAの原文を確認してみましょう。Fowlerは Transaction Script について以下のように述べています。
Transaction Script organizes business logic by procedures where each procedure handles a single request from the presentation.
つまり、プレゼンテーション層からの1リクエストに対して1つの手続きでビジネスロジックを処理するパターンです。そして重要なのは、Fowlerがこのパターンを「悪」とは断じていないことです。
The glory of Transaction Script is its simplicity. Organizing logic this way is natural for applications with only a small amount of logic, and it involves very little overhead either in performance or in understanding.
「シンプルさこそが Transaction Script の栄光である」と明言しています。ロジックが少量のアプリケーションでは、これが自然な選択であり、パフォーマンス面でも理解の面でもオーバーヘッドが少ないのです。
では、いつ Domain Model に移行すべきか
Fowlerは明確な判断基準も示しています。
As the business logic gets more complicated, however, it gets progressively harder to keep it in a well-designed state. One particular problem to watch for is its duplication between transactions. Since the whole point is to handle one transaction, any common code tends to be duplicated.
ビジネスロジックが複雑になるにつれて、良い設計を維持することが難しくなります。特に注意すべき問題は、トランザクション間でのコードの重複です。
整理すると、ロジックが単純なら Transaction Script、ロジックが複雑になり重複が増えてきたら Domain Model を検討する、という判断になります。この判断は事前に決められるものではなく、システムの成長に伴って見直すべきものです。
ECサイトでの具体例
業務の性質によって適切なパターンは異なります。ECサイトを例に考えてみましょう。
Transaction Script が適するケース
// 商品情報を更新する処理
async function updateProductInfo(productId: string, input: UpdateProductInput): Promise<void> {
const product = await productRepository.findById(productId);
if (!product) throw new ProductNotFoundError(productId);
product.name = input.name;
product.description = input.description;
product.price = input.price;
product.updatedAt = new Date();
await productRepository.save(product);
await auditLogRepository.log('product_updated', productId);
}
この処理は単純です。入力を受け取り、商品レコードを更新し、監査ログを残します。ここに複雑なドメインロジックは存在しません。Domain Model を導入しても得られるものは少ないでしょう。
Domain Model が必要になるケース
// 注文処理
class Order {
checkout(customer: Customer, paymentMethod: PaymentMethod): CheckoutResult {
if (!this.hasItems()) {
return CheckoutResult.emptyCart();
}
if (!this.allItemsInStock()) {
return CheckoutResult.outOfStock(this.getOutOfStockItems());
}
if (!customer.canPurchase(this.totalAmount)) {
return CheckoutResult.creditLimitExceeded();
}
const discount = this.calculateDiscount(customer);
this.applyDiscount(discount);
this.status = OrderStatus.CONFIRMED;
this.confirmedAt = new Date();
return CheckoutResult.success(this.totalAmount);
}
}
注文処理には複雑なルールがあります。在庫チェック、与信確認、割引計算。これらのルールが絡み合い、複数の箇所で同じ判断が必要になります。こうした場合、ドメインオブジェクトにロジックを凝集させることで、重複を排除し、変更に強い設計が実現できます。
「CRUDの8割はTransaction Scriptで十分」という仮説
実際のエンタープライズアプリケーションを見ると、多くの機能は以下のパターンに収まります。
- 入力を受け取る
- バリデーションする
- データベースに保存する
- 結果を返す
この種の処理に Domain Model を適用すると、過剰な抽象化になりがちです。
私の経験則では、アプリケーションの機能の約8割はこのシンプルなパターンで十分であり、残り2割の複雑なビジネスロジックにこそ Domain Model を適用すべきだと考えています。
Service層の肥大化は設計の失敗か
「Service層が肥大化した」という悩みをよく聞きます。これを「Transaction Script への回帰だ」と批判する声もありますが、本当にそうでしょうか。
Service層が肥大化する原因は、多くの場合以下のいずれかです。
- 本来ドメインに属するロジックがServiceに漏れ出している → Domain Model の導入を検討
- 単純にアプリケーションが大きくなった → Service の分割を検討
- トランザクション境界の管理が複雑になった → これは別の問題
重要なのは、「Service層が大きい=悪」という短絡的な判断をしないことです。そのServiceが Transaction Script として適切に機能しているなら、無理に Domain Model に移行する必要はありません。
トレードオフを見極める
| 観点 | Transaction Script | Domain Model |
|---|---|---|
| 学習コスト | 低い | 高い |
| 初期開発速度 | 速い | 遅い |
| ロジックの重複 | 起きやすい | 起きにくい |
| 変更への強さ | 弱い | 強い |
| テスト容易性 | 統合テスト寄り | ユニットテスト可能 |
どちらが優れているかではなく、今のプロジェクトにとって何が重要かで判断すべきです。
まとめ
Fowlerは Transaction Script を否定していません。むしろ、適切な状況では最もシンプルで効果的なパターンとして推奨しています。
現代のソフトウェア開発において重要なのは、パターンの適用条件を理解し、二項対立を避け、進化を許容することです。「Domain Model を使わないのは技術的負債だ」という言説に惑わされず、Fowlerが語った原則に立ち返りましょう。シンプルなことをシンプルに保つことも、優れた設計の一つの形です。
参考文献
- Martin Fowler『Patterns of Enterprise Application Architecture』(2002)
- Anemic Domain Model