Transaction Scriptは本当にアンチパターンか

「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で十分」という仮説

実際のエンタープライズアプリケーションを見ると、多くの機能は以下のパターンに収まります。

  1. 入力を受け取る
  2. バリデーションする
  3. データベースに保存する
  4. 結果を返す

この種の処理に Domain Model を適用すると、過剰な抽象化になりがちです。

私の経験則では、アプリケーションの機能の約8割はこのシンプルなパターンで十分であり、残り2割の複雑なビジネスロジックにこそ Domain Model を適用すべきだと考えています。

Service層の肥大化は設計の失敗か

「Service層が肥大化した」という悩みをよく聞きます。これを「Transaction Script への回帰だ」と批判する声もありますが、本当にそうでしょうか。

Service層が肥大化する原因は、多くの場合以下のいずれかです。

  1. 本来ドメインに属するロジックがServiceに漏れ出している → Domain Model の導入を検討
  2. 単純にアプリケーションが大きくなった → Service の分割を検討
  3. トランザクション境界の管理が複雑になった → これは別の問題

重要なのは、「Service層が大きい=悪」という短絡的な判断をしないことです。そのServiceが Transaction Script として適切に機能しているなら、無理に Domain Model に移行する必要はありません。

トレードオフを見極める

観点Transaction ScriptDomain Model
学習コスト低い高い
初期開発速度速い遅い
ロジックの重複起きやすい起きにくい
変更への強さ弱い強い
テスト容易性統合テスト寄りユニットテスト可能

どちらが優れているかではなく、今のプロジェクトにとって何が重要かで判断すべきです。

まとめ

Fowlerは Transaction Script を否定していません。むしろ、適切な状況では最もシンプルで効果的なパターンとして推奨しています。

現代のソフトウェア開発において重要なのは、パターンの適用条件を理解し、二項対立を避け、進化を許容することです。「Domain Model を使わないのは技術的負債だ」という言説に惑わされず、Fowlerが語った原則に立ち返りましょう。シンプルなことをシンプルに保つことも、優れた設計の一つの形です。

参考文献