💭

トランザクションスクリプトはだめなのか?

2024/07/23に公開

トランザクションスクリプトはだめなのか?

トランザクションスクリプトのことをだめなコード (可読性が低く、修正が難しく、修正で壊れやすい) の代名詞として利用している人がそれなりにいる気がします。私自身はトランザクションスクリプトだからだめだとは思っていません。実際のところどうなん? ってことで、久々にエンタープライズアプリケーションアーキテクチャパターン (PoEAA) を取り出して調べてみました。

トランザクションスクリプトはアンチパターンではない

トランザクションスクリプトはアンチパターンではありません。PoEAA でもアンチパターンとして取り上げられてはいません。

P.116 より

シンプルな場合には、ロジックをどのように体系化するのかまでは必要ない。もちろん、どのようなプログラムでも、ある程度合理的なモジュールとしてコードを構成する必要がある。

トランザクションスクリプトであっても、処理が複雑になのであれば、モジュールに分離する必要があると読めます。つまり、トランザクションスクリプトはべた書きのコードのことだけを指しているわけではありません。

同じく P.116 より

また、トランザクションスクリプトをどこに置くかは、レイヤをどのように体系化するかによって異なる。サーバページ、CGI スクリプト、または分散セッションオブジェクトのいずれの中にでも置くことができる。私はできる限りトランザクションスクリプトを分離させることを好んでいる。

トランザクションスクリプトの実装を MVC のコントローラーに直接書くこともできますが、著者は専用のクラスに書くことを好んでいるように読めます。つまり、トランザクションスクリプトはコントローラーに直接実装を書くことだけを指しているわけではありません。

次のコードは書籍の中で紹介されているトランザクションスクリプトのコードを C# + ADO.NET (書籍では Java + JDBC) に書き換えたものです。

public void CalculateRevenueRecognitions(long contractNumber)
{
    var contracts = db.FindContract(contractNumber);
    contracts.Read();
    var totalRevenue = Money.Dollars(contracts.GetDecimal(0));
    var recognitionDate = new MfDate(contracts.GetDateTime(1));
    var type = contracts.GetString(2);
    if (type == "S")
    {
        var allocation = totalRevenue.Allocate(3);
        db.InsertRecognition(contractNumber, allocation[0], recognitionDate);
        db.InsertRecognition(contractNumber, allocation[1], recognitionDate.AddDate(60));
        db.InsertRecognition(contractNumber, allocation[2], recognitionDate.AddDate(90));
    }
    else if (type == "W")
    {
        db.InsertRecognition(contractNumber, totalRevenue, recognitionDate);
    }
    else if (type == "D")
    {
        var allocation = totalRevenue.Allocate(3);
        db.InsertRecognition(contractNumber, allocation[0], recognitionDate);
        db.InsertRecognition(contractNumber, allocation[1], recognitionDate.AddDate(30));
        db.InsertRecognition(contractNumber, allocation[2], recognitionDate.AddDate(60));
    }
}

コードを読んでみると、次のような特徴があります。

  • データベースへのクエリ発行は別のクラス Gateway に委譲しているが、トランザクションスクリプト内でデータベース関連クラス (IDataReader) を直接利用している
  • 製品の種別 (S / W / D) での収益計上のやり方のビジネスロジックは、トランザクションスクリプトに直接書かれている (if (type == ".") のところ)
  • 金額を三分割 (100 円を 34 / 33 / 33 に分割する) などのビジネスロジックは Money バリューオブジェクトに実装している (サンプルからは省略している)

つまり、トランザクションスクリプトでも次のようなことはやっていいわけです。

  • データベース操作など、用途に応じてモジュールを分割すること
  • バリューオブジェクトを導入して、共通のビジネスロジックを抽出すること
  • 複雑度に応じて、よりたくさんのモジュールに分割すること

というか、書籍ではこれらを推奨しています。トランザクションスクリプトであっても、可読性が高く修正で壊れにくいコードを書くのは可能ですし、それを目指す必要があります。

複雑だからドメインモデルを採用すべき?

P.122 より

一般に使われる RevenueRecognition ルールは、実際にはもっと複雑で Product ごとに違いがあるだけでなく、日付によっても異なる (...省略)。ルールが複雑になると、トランザクションスクリプトでは、一貫した設計を維持できなくなってしまう。そのため、私のようなオブジェクト指向主義者は、このような状況ではドメインモデルを好んで使うのである。

とありますので、筆者としては今回のサンプルコード程度では問題ないけど、現実はもっと複雑なので、ドメインモデルを使った方がいいとしています。(もちろん、シンプルな要件ではトランザクションスクリプトの方が優れているとも言っています)

少し戻って P.117 より

1 つのトランザクションを処理することが重要事項となるので、共通するどのコードも重複しやすい状況になる。
きめ細かにファクタリングすると問題の多くは軽減できるが、より複雑なビジネスドメインでは、ドメインモデルの構築が必要になる。

とありますので、トランザクションスクリプトでもファクタリング (プログラムを細かく分割する) が必要で、複雑さに応じてモジュールを分割していく必要があります。ですが、それには限界があり、ドメインモデルの方がより複雑なビジネスロジックに向いていると読めます。

ところで、複雑だからドメインモデルを採用すべきでしょうか? トランザクションスクリプトもドメインモデルもマスターした開発者ならそうだと思います。しかし、重要なことは可読性や修正で壊れにくいことです。それが達成できるならトランザクションスクリプトでも問題はありません。複雑であってもモジュールに分割していけばいいわけです。

逆にドメインモデルを採用したからと言って、自動で複雑な要件に対処できるようにはなりません。修正で壊れやすいコードを書く開発者が、ドメインモデルを採用したとしても、ただのデータクラスにしかならず、ビジネスロジックは分散し、修正で壊れやすいのは変わらずに、可読性の低下まで招くでしょう。

ドメインモデル + サービスレイヤ

ドメインモデルだけではアプリケーションの機能は提供できず、それをサービスとして公開するために書籍ではサービスレイヤの導入を紹介しています。

P.143 より (サービスレイヤ)

私も含めて設計者の多くは、「ビジネスロジック」を 2 種類に分離する傾向がある。1 つは、純粋に問題ドメイン (Contract の RevenueRecognition を算出するストラテジーなど) を扱う「ドメインロジック」で、もう 1 つは、アプリケーションの責任 (RevenueRecognition 計算における Contract 管理者への通知やアプリケーションの統合) を扱う「アプリケーションロジック」である。

操作スクリプト手法では、厚いクラス群としてサービスレイヤが実装され、アプリケーションロジックは直接実装されるが、ドメインロジックはカプセル化されたドメインクラスに委譲される。サービスレイヤのクライアントが利用できる操作はスクリプトとして実装され、関連するロジックの対象エリアを定義するクラスに一部の操作が体系化される。

ちょっと部分的な引用ではわかりにくいのですが、ドメインロジックはドメインモデルがいいけど、アプリケーションロジックはスクリプト的に書いた方がいいと読めます。

ドメインモデルそのものはドメインロジックの提供が目的なので、そのままではサービスとして公開できません。それをサービスとして公開するためのサービスレイヤですが、サービスレイヤの実装はトランザクションスクリプトによく似ていることになります。もちろん、ドメインロジックはドメインモデルに委譲されていますが。

なお、書籍の中ではサービスレイヤのことをトランザクションスクリプトとは呼んでいません。

よくできたトランザクションスクリプトはドメインモデルと区別がつかないのではないか

これは完全に私見なのですが...

トランザクションスクリプトで書いたとしても、単一責任の原則や凝集度などの原則を突き詰めていけば、自然とデータベースアクセスとビジネスロジックなどはそれぞれ分離されていくと思っています。それに加えて、オブジェクト指向のメリットを生かす設計をすれば、ビジネスロジックはドメインモデルと見分けがつかなくなるでしょう。

なので、よくできたトランザクションスクリプトはドメインモデルと区別があまりつかなくなると考えています。

まあ、べた書きで書いたトランザクションスクリプトからよくできたドメインモデルに移行できるようなレベルの人は、最初からドメインモデルで設計すると思います。

ところで、よくできたトランザクションスクリプトがドメインモデルに近づくとして、どこまでがトランザクションスクリプトでどこからがドメインモデル + サービスレイヤなのでしょうか? 書籍ではサービスレイヤはスクリプト的になるとは書かれていましたが、トランザクションスクリプトとは書かれていません。

P.124 より

一方、豊富なドメインモデルは見た目もデータベース設計とは異なり、継承、ストラテジー、その他の [Gang of Four] パターンや、さらには相互に関連する小さなオブジェクトが複雑に絡み合う構造を持っている。

とあります。トランザクションスクリプトは構造化プログラミングの考え方で、ドメインモデルはオブジェクト指向プログラミングの考え方のようです。境目は自分にはわかりません。

(こういう線引きは酒の肴としてはとても楽しいのですが、現実のプログラミングにはそれほど関係がないのでこの辺で...)

最後に

トランザクションスクリプト自体が悪いと思いません。要件が複雑だからドメインモデルでなければいけないとも思いません。重要なことは、可読性や修正の難易度や修正での壊れにくさを担保できるかどうかです。これらを担保する難易度は要件が複雑になればなるほど非線形で上昇します。

トランザクションスクリプトをだめなコードの代名詞のように使うと、目の前のコードの問題を隠してしまいます。例えばトランザクションスクリプトで書かれた 1,000 行の関数は、トランザクションスクリプトだから 1,000 行になったわけではありません。リファクタリングや設計を見直さなかった (もしくはそうしなかった) 開発者のツケです。その状態で、問題をトランザクションスクリプトのせいにしてドメインモデルを採用しても、根本的な問題は何一つ解決しないでしょう。

トランザクションスクリプトをだめなコードの代名詞として利用するのをやめませんか?

Discussion