次の方法で共有


実践的なパターン

Unit of Work パターンと永続性の無視

Jeremy Miller

目次

Unit of Work パターン
Unit of Work を使用する
永続性の無視 (Persistence Ignorance)
データベースとは無関係にビジネス ロジックを実行できるか
データベース モデルとは無関係にドメイン モデルを設計できるか
ビジネス ロジックに対する永続化戦略の影響
Unit of Work に関するその他の考慮事項

2009 年 4 月号の MSDN マガジン (「永続化のパターン」) では、ある種のオブジェクト/リレーショナル マッピング (O/RM) テクノロジを使用してビジネス エンティティ オブジェクトを表すときの一般的なパターンをいくつか紹介しました。独自の O/RM ツールを最初から作成することはあまりないでしょうが、既存のツールを効果的に使用するには (または単に選択するだけであっても)、これらのパターンを知っておくことが重要です。

この記事では、次の永続化パターンである Unit of Work 設計パターンについて説明し、さらに永続性の無視 (Persistence Ignorance) にまつわる問題について調べます。この記事では、問題領域の例として一般的な請求システムを使用します。

Unit of Work パターン

エンタープライズ ソフトウェア開発で最もよく使用される設計パターンの 1 つは Unit of Work です。Martin Fowler によると、Unit of Work パターンでは「ビジネス トランザクションによって影響を受けるオブジェクトのリストを保持し、変更の書き込みと同時実行の問題の解決を調整します」。

Unit of Work パターンは必ずしも自分ではっきりと意識して作成するようなものではありませんが、私が知っているほとんどすべての永続化ツールで使用されています。NHibernate の ITransaction インターフェイス、LINQ to SQL の DataContext クラス、Entity Framework の ObjectContext クラスなどはすべて、Unit of Work の例です。ついでにいえば、昔からある DataSet も Unit of Work として使用できます。

場合によっては、アプリケーション固有の Unit of Work インターフェイスまたはクラスを独自に作成し、永続化ツールの Unit of Work を内部にラップできます。これにはさまざまな理由があります。たとえば、アプリケーション固有のログ、トレース、またはエラー処理をトランザクション管理に追加する場合があります。おそらく、永続化ツールの細部を、アプリケーションの他の部分からカプセル化する必要があります。このような余分なカプセル化を施しておくと、後で永続化テクノロジを簡単に交換できます。または、システムのテストの容易性を高めることもできます。一般的な永続化ツールに組み込まれている Unit of Work の実装の多くは、自動化された単体テストのシナリオでは簡単に扱うことができません。

Unit of Work の実装を独自に作成するとしたら、おそらく次のようなインターフェイスになります。

public interface IUnitOfWork {
  void MarkDirty(object entity);
  void MarkNew(object entity);
  void MarkDeleted(object entity);
  void Commit();
  void Rollback();
}

Unit of Work クラスには、エンティティに変更済み、新規、または削除済みというマークを付けるメソッドを用意します (多くの実装では、Unit of Work 自体に変更されたエンティティを自動的に判別するなんらかの手段があるので、MarkDirty を明示的に呼び出す必要はありません)。Unit of Work には、すべての変更をコミットまたはロールバックするメソッドも作成します。

ある意味、Unit of Work はすべてのトランザクション処理コードを詰め込む場所と考えることができます。Unit of Work では次のようなことを行います。

  • トランザクションの管理。
  • データベースの挿入、削除、更新の指示。
  • 重複更新の防止。Unit of Work オブジェクトを 1 回使用するコードの内部で、複数箇所で同じ Invoice オブジェクトが変更済みとマークされる場合がありますが、Unit of Work クラスがデータベースに UPDATE コマンドを発行するのは 1 回だけです。

Unit of Work パターンを使用することの利点は、コードの他の部分がこれらの処理から解放されて、ビジネス ロジックに集中できることです。

Unit of Work を使用する

Unit of Work パターンを使用する最適な方法の 1 つは、異なるクラスとサービスを単一の論理的なトランザクションにまとめることです。ここで重要なポイントは、異なるクラスとサービスを、相互を意識させないまま、単一のトランザクションに参加させることができるということです。従来、このようなことは、MTS/COM+ などのトランザクション コーディネータや、もっと新しい System.Transactions 名前空間を使用することで可能でした。個人的には、Unit of Work パターンを使用して関係のないクラスとサービスを論理トランザクションに参加させる方法が気に入っています。その方が、コードがいっそう明確で理解しやすくなり、単体テストも簡単になるためです。

新しい請求システムでは、請求ライフサイクルのさまざまな時点で既存の請求に対して異なる処理を実行するものとします。この処理はたびたび変更され、請求処理の新規追加や削除が頻繁に発生するので、Command パターンを適用し (「Command パターン、MSMQ、および .NET を使用して分散システムの設計を簡単にする」を参照)、Invoice に対する 1 つの個別処理を表す IInvoiceCommand という名前のインターフェイスを作成します。

public interface IInvoiceCommand {
  void Execute(Invoice invoice, IUnitOfWork unitOfWork);
}

IInvoiceCommand インターフェイスには Execute という簡単なメソッドが含まれ、Invoice と IUnitOfWork オブジェクトを使用してなんらかの処理を実行するために呼び出されます。すべての IInvoiceCommand オブジェクトは、IUnitOfWork 引数を使用して、その論理トランザクション内で変更をデータベースに保存する必要があります。

非常に簡単ですが、この Command パターンと Unit of Work パターンの組み合わせが本当におもしろくなるのは、複数の IInvoiceCommand オブジェクトをまとめてからです (図 1 を参照)。

図 1 IInvoiceCommand の使用

public class InvoiceCommandProcessor {
  private readonly IInvoiceCommand[] _commands;
  private readonly IUnitOfWorkFactory _unitOfWorkFactory;

  public InvoiceCommandProcessor(IInvoiceCommand[] commands, 
    IUnitOfWorkFactory unitOfWorkFactory) {

    _commands = commands;
    _unitOfWorkFactory = unitOfWorkFactory;
  }

  public void RunCommands(Invoice invoice) {
    IUnitOfWork unitOfWork = _unitOfWorkFactory.StartNew();

    try {
      // Each command will potentially add new objects
      // to the Unit of Work for insert, update, or delete
      foreach (IInvoiceCommand command in _commands) {
        command.Execute(invoice, unitOfWork);
      }

      unitOfWork.Commit();
    }
    catch (Exception) {
      unitOfWork.Rollback();
    }
  }
}

Unit of Work によるこの方法を使用すると、IInvoiceCommand の異なる実装をうまく組み合わせて請求システムのビジネス ルールを追加または削除しながら、トランザクションとしての整合性を保つことができます。

私の経験では、実務担当者は延滞や未払いの請求を非常に気にするので、Invoice が延滞と判断された時点で担当者に警告する新しい IInvoiceCommand クラスを作成することがおそらく必要です。このルールの実装の例を次に示します。

public class LateInvoiceAlertCommand : IInvoiceCommand {
  public void Execute(Invoice invoice, IUnitOfWork unitOfWork) {
    bool isLate = isTheInvoiceLate(invoice);
    if (!isLate) return;

    AgentAlert alert = createLateAlertFor(invoice);
    unitOfWork.MarkNew(alert);
  }
}

この設計の美しいところは、LateInvoiceAlertCommand の開発とテストを、データベースさらには同じトランザクションに含まれる他の IInvoiceCommand オブジェクトから完全に独立して行うことができる点です。最初に、IInvoiceCommand オブジェクトと Unit of Work の相互作用をテストするため、テストを正しく行うためだけに使用する IUnitOfWork の模擬の実装を作成します。StubUnitOfWork を記録スタブと呼びます。

public class StubUnitOfWork : IUnitOfWork {
  public bool WasCommitted;
  public bool WasRolledback;
  public void MarkDirty(object entity) {
    throw new System.NotImplementedException();
  }
  public ArrayList NewObjects = new ArrayList();
  public void MarkNew(object entity) {
    NewObjects.Add(entity);
  }
}

データベースとは関係なく動作する優れた模擬 Unit of Work ができたので、LateInvoiceAlertCommand 用のテスト フィクスチャは図 2 のようなコードになります。

図 2 LateInvoiceAlertCommand 用のテスト フィクスチャ

[TestFixture]
public class 
  when_creating_an_alert_for_an_invoice_that_is_more_than_45_days_old {

  private StubUnitOfWork theUnitOfWork;
  private Invoice theLateInvoice;

  [SetUp]
  public void SetUp() {
    // We're going to test against a "Fake" IUnitOfWork that 
    // just records what is done to it
    theUnitOfWork = new StubUnitOfWork();

    // If we have an Invoice that is older than 45 days and NOT completed
    theLateInvoice = new Invoice {InvoiceDate = 
      DateTime.Today.AddDays(-50), Completed = false};

      // Exercise the LateInvoiceAlertCommand against the test Invoice
      new LateInvoiceAlertCommand().Execute(theLateInvoice, theUnitOfWork);
  }

  [Test]
  public void 
    the_command_should_create_a_new_AgentAlert_with_the_UnitOfWork() {

    // just verify that there is a new AgentAlert object
    // registered with the Unit of Work
    theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
  }

  [Test]
  public void the_new_AgentAlert_should_have_XXXXXXXXXXXXX() {
    var alert = theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
    // verify the actual properties of the new AgentAlert object
    // for correctness
  }
}

永続性の無視 (Persistence Ignorance)

プロジェクトの永続化ソリューションを選択または設計するとき、私はビジネス ロジックのコードに対する永続化インフラストラクチャの影響に留意します。少なくとも、ビジネス ドメインに関するロジックが多いシステムではそのようにします。ビジネス ロジックの設計、作成、テストをデータベースおよび永続化インフラストラクチャのコードからある程度独立して行うことができれば理想的です。特に、永続性の無視、または Plain Old CLR Object (POCO) の考え方をサポートするソリューションが必要です。

まず、「永続性の無視」とはどのようなことで、何が基になっているのでしょうか。Jimmy Nilsson は自著『ドメイン駆動』(翔泳社、2008 年) の中で次のように POCO を定義しています。「... インフラストラクチャ関連の理由による余計なものが追加されていない、近い将来のビジネス上の問題に焦点を当てた普通のクラス。... クラスは身近なビジネスの問題に焦点を当てる必要があります。ドメイン モデルでは、他のものをクラスに入れるべきではありません。」

それでは、なぜ留意する必要があるのでしょうか。説明の最初にまずはっきりさせる必要があるのは、永続性の無視とはさまざまな設計目標に対する手段でしかなく、最終的な目標それ自体ではないということです。新しい永続化ツールを評価するとき、私は普通、次に示す質問を自問自答します。質問は重要なものから順に示してあります。永続性の無視はこれらの質問が示す条件を満たすためにどうしても必要なものではありませんが、永続性の無視に基づくソリューションの方が通常、インフラストラクチャの機能をビジネス オブジェクトに埋め込む必要があるソリューションより優れています。

データベースとは無関係にビジネス ロジックを実行できるか

私にとってこれは最も重要な問題です。ソフトウェア プロジェクトが成功する最も重要な要因の 1 つは、迅速なフィードバック サイクルを使用できることです。つまり、"何か新しいものをコーディングした" 時点から、"新しいコードが動作することが証明されたので他のものに移る" または "新しいコードに問題があるのですぐに修正する" までの時間と作業を短縮したいのです。

私の経験では常に、単体テストが簡単なアーキテクチャでは、またはアプリケーション全体を実行しないで新しい小さなコードを実行できるだけのアーキテクチャであっても、チームの生産性ははるかに高くなります。逆に、ビジネス ロジックとインフラストラクチャの実行時の結び付きが強いアーキテクチャでの開発は非常に困難です。

45 日を過ぎても未払いの請求がある場合に新しい担当者警告を作成するというビジネス ルールの場合を再び考えてみましょう。後になって、45 日ではなく 30 日で警告を作成することが必要になりました。請求警告ロジックに対する変更を検証するには、未処理と処理済みの請求および 30 日のしきい値より古い請求または新しい請求に対してこのルールを適用する状態にコードを変更する必要があります。ここから先、アーキテクチャが問題になります。新しい請求警告ロジックまで進んで新しいビジネス ルールの単体テストを実行できるでしょうか、それとも最初に技術的な障害に対応する必要があるでしょうか。

迅速なフィードバック サイクルの観点から最も望ましくないのは、インフラストラクチャが存在しないとビジネス ロジックが機能できないモデルです。たとえば、私が最初に作成した O/RM では、次のようなエンティティ クラスを作成する必要がありました。

public class Invoice : MySpecialEntityType {
  private long _id;
  public Invoice(long id) {
    _id = id;
    // Read in the rest of the Invoice information 
    // from the database
    loadDataFromDatabase(id);
  }
  public Invoice() {
    // Hit the database 
    _id = fetchNextId();
  }
}

このような設計では、データベースに接続していないと Invoice クラスを作成できません。元の設計では、データベース エンジンが正しく構成されて使用できる状態でないと、ビジネス エンティティ クラスを使用または実行する簡単な方法はありませんでした。

確かにデータベースにアクセスするコードの自動テストを作成することはできますが、データベースに依存しないオブジェクトの自動テストの作成と比較して、開発者がテスト データの設定に要する作業時間ははるかに多くなります。簡単な請求をデータベースに作成するだけでも、対象のビジネス ルールとは関係のない、非 null フィールドや、参照整合性要件を満たすデータを用意する作業に、取り組む必要があります。

代わりに、コードで "31 日経過した未払いの請求がある場合は、…" と指定するだけで済めば、どれほどいいでしょう。そのために、次のようなさらに Persistence-Ignorant な方法に Invoice クラスを移動してみてはどうでしょう。

// The Invoice class does NOT depend on 
// any sort of persistence infrastructure
public class Invoice {
  private long _id;

  public Invoice() { }

  public bool IsOpen { get; set; }
  public DateTime? InvoiceDate { get; set; }
}

このようにすると、延滞請求警告ルールをテストするときは、次のようなテスト フィクスチャの簡単なコードで、31 日経過した未払い請求をテスト シナリオ用に短時間で作成できます。

 [SetUp]
public void SetUp() {
  Invoice theLateInvoice = new Invoice() {
    InvoiceDate = DateTime.Today.AddDays(-31),
    IsOpen = true
  };
}

この方法では、延滞請求ルールのビジネス ロジック コードと Invoice クラス自体を、永続化コードから分離しています。請求データをメモリ内に簡単に作成できれば、ビジネス ルールの単体テストを迅速に行うことができます。

余談ですが、既存の永続化ツールを選択するときは、遅延読み込みの実装方法に注意してください。一部のツールでは、Virtual Proxy パターンで透過的な遅延読み込みが実装されており、実質的にビジネス エンティティ自体から遅延読み込みが見えなくなっています。他のツールでは、遅延読み込みのサポートをビジネス エンティティに直接埋め込むコード生成技法が使用されており、実質的にエンティティから永続化インフラストラクチャに対する実行時の緊密な結び付きが作成されます。

開発者が自動テストの作成にかかる時間と同じくらい重要なのが、テストの実行速度です。データ アクセスまたは Web サービス アクセスが関係する自動テストは、完全に 1 つの AppDomain 内で実行できるテストに比べて、実行が 1 桁以上遅くなります。これは大きな問題ではないように見えますが、プロジェクトの規模は時間が経つと大きくなるので、自動テストの実行速度が遅いと簡単にチームの生産性が低下し、そもそも自動テストの有用性がなくなります。

データベース モデルとは無関係にドメイン モデルを設計できるか

実行時にデータベースからビジネス層を切り離すことができると、オブジェクトの構造をデータベース スキーマから (またはその逆) 独立して設計できるかどうかという問題に直面します。オブジェクト モデルは、ビジネス ロジックの必要な動作に基づいて設計できる必要があります。一方、データベースは、データの読み書きの効率性、および参照整合性の適用を考慮して設計する必要があります (ところで、O/RM ツールを使用するには参照整合性が不可欠です)。

ドメイン モデルをデータベースの構造から分けることができる典型的なシナリオを示すため、事例の分野をエネルギー取引システムに変えましょう。このシステムは、売買および配送される石油の量を追跡して価格を設定します。単純化したシステムでは、取引には購入された複数の数量と配送された数量の記録を含めることができます。ただし、ここで問題があります。数量は計量の単位とその単位の数を意味します。この取引システムの場合、3 つの異なる単位で数量を追跡するものとします。

public enum UnitOfMeasure {
  Barrels,
  Tons,
  MetricTonnes
}  

ドメイン モデルをデータベース スキーマのフラットな構造に固定しようとすると、クラスは次のようになります。

public class FlatTradeDetail {
  public UnitOfMeasure PurchasedUnitOfMeasure {get;set;}
  public double PurchasedAmount {get;set;}
  public UnitOfMeasure DeliveredUnitOfMeasure { get; set; }
  public double DeliveredAmount { get; set; }
}

この構造はデータベースの構造に適しており、既存のデータベースからコードの構造を生成するソリューションには便利ですが、ビジネス ロジックを実行するのには適していません。

エネルギー取引システムのビジネス ロジックの多くの部分では数量の比較、減算、加算が必要ですが、常に計量の単位が異なることを想定し、比較を行う前に単位の変換を行う必要があります。私自身の手痛い経験からいうと、データベース スキーマから直接生成されたフラットな構造では、このロジックを実装するのは困難です。

フラットな構造の代わりに、Money パターンを実装し、Quantity の動作をモデル化するクラスを作成してみます。図 3 にこれを示します。

図 3 Quantity

public class Quantity {
  private readonly UnitOfMeasure _uom;
  private readonly double _amount;

  public Quantity(UnitOfMeasure uom, double amount) {
    _uom = uom;
    _amount = amount;
  }

  public Quantity ConvertTo(UnitOfMeasure uom) {
    // return a new Quantity that represents
    // the equivalent amount in the new
    // Unit of Measure
  }

  public Quantity Subtract(Quantity other) {
    double newAmount = _amount - other.ConvertTo(_uom).Amount;
    return new Quantity(_uom, newAmount);
  }

  public UnitOfMeasure Uom {
    get { return _uom; }
  }

  public double Amount {
    get { return _amount; }
  }
}

Quantity クラスを使用して計量の単位に関係する一般的な動作をモデル化および再利用すると、TradeDetail クラスは次のようになります。

public class TradeDetail     {
  private Quantity _purchasedQuantity;
  private Quantity _deliveredQuantity;
  public Quantity Available() {
    return _purchasedQuantity.Subtract(_deliveredQuantity);
  }
  public bool CanDeliver(Quantity requested) {
    return Available().IsGreaterThan(requested);
  }
}

Quantity クラスを使用すると TradeDetail のロジックの実装は確実に簡単になりますが、オブジェクト モデルとデータベースの構造は一致しなくなります。永続化ツールがこの種のマッピングをサポートしているのが理想的です。

オブジェクト モデルとデータベース モデルの違いによる問題は、通常、単純なビジネス ロジックのシステムでは発生しません。このようなシステムでは、データベース構造からエンティティ クラスを単純に生成する Active Record アーキテクチャが最も簡単なソリューションである可能性があります。または、エンティティ オブジェクトからデータベース スキーマを生成できるデータベース マッパー ツールを使用することもできます。

ビジネス ロジックに対する永続化戦略の影響

完全な世界などありません。どのような永続化ツールも、エンティティ クラスの設計方法になんらかの影響を及ぼします。たとえば、私のチームは永続化に NHibernate を使用しています。NHibernate での遅延読み込みされるプロパティの関係の実装方法のため、プロパティの多くは、次のような Invoice での新しい Customer プロパティのように、遅延読み込みを可能にするためだけに、virtual と指定する必要があります。

public class Invoice {
  public virtual Customer Customer { get; set; }
}

Customer プロパティが Virtual として指定されているのは、遅延読み込みの Customer プロパティを提供する Invoice オブジェクトに対して NHibernate が動的プロキシを作成できるようにするためだけです。

遅延読み込みをサポートするためだけにメンバを virtual として指定するのは面倒なこと程度で済みますが、さらに深刻な問題が発生する可能性があります。ドメイン駆動設計 (DDD) を使用するチームの数は増えています。DDD での共通戦略の 1 つは、無効な状態にすることのできないドメイン モデル クラスを作成することです。ドメイン モデル クラス自体でも、ビジネス ルールが内部的に適用されるように用心深く内部データを保護します。この設計方針を実現しようとすると、Invoice クラスは図 4 のようになります。

図 4 ドメイン モデルの Invoice クラス

public class Invoice {
  private readonly DateTime _invoiceDate;
  private readonly Customer _customer;
  private bool _isOpen;

  // An Invoice must always have an Invoice Date and
  // a Customer, so there is NO default constructor
  // with no arguments
  public Invoice(DateTime invoiceDate, Customer customer) {
    _invoiceDate = invoiceDate;
    _customer = customer;
  }

  public void AddDetail(InvoiceDetailMessage detail) {
    // determines whether or not, and how, a
    // new Invoice Detail should be created 
    // and added to this Invoice
  }

  public CloseInvoiceResponse Close(CloseInvoiceRequest request) {
    // Invoice itself will determine if the correct
    // conditions are met to close itself.
    // The _isOpen field can only be set by
    // Invoice itself
  }

  public bool IsOpen {
    get {
      return _isOpen;
    }
  }

  public DateTime InvoiceDate {
    get { return _invoiceDate; }
  }

  public Customer Customer {
    get { return _customer; }
  }
}

このドメイン モデル作成方法はすべての種類のアプリケーションに適用できるわけではありませんが、このスタイルのアーキテクチャを選択した場合、永続化ツールの選択が影響を受けます。

図 4 の形式の Invoice にはただ 1 つのコンストラクタ関数があり、Invoice Date と Customer がない場合は Invoice を作成できないというルールを適用します。ほとんどではないにしても多くの永続化テクノロジでは、引数のないコンストラクタ関数が必要です。

また、このバージョンの Invoice クラスでは、_isOpen のような内部フィールドを用心深く保護するために、パブリックな setter プロパティが用意されていません。やはり、多くの永続化ツールはパブリックなプロパティに対してのみ動作します。より厳密な DDD スタイルのエンティティを使用する場合は、マッピング フィールド、プライベート プロパティ、または引数のあるコンストラクタ関数を永続化ツールがサポートできるかどうかを調査する必要があります。

Unit of Work に関するその他の考慮事項

Unit of Work パターンの使用に関しては、考慮する価値のある重要な問題が他にもいくつかあります。Unit of Work パターンをアプリケーションで明示的に使用することに関心がある場合は、アプリケーションのコンテキストでこれらの問題を調査する必要があります。

  • リポジトリと Unit of Work の機能の分担。すべての読み取りはリポジトリを通して行い、書き込みは Unit of Work を通して行うと考えている読者もいるでしょう。状態変化を簡単に追跡できるように Unit of Work を通して読み取りとクエリを行うように強制する Unit of Work の実装を使用する場合もあります。
  • 論理トランザクションに参加するさまざまなクラスに、正しい Unit of Work と基になっている Identity Map ヘルパを提供する方法。多くの場合、現在の HttpContext、スレッド、またはその他のスコープ戦略に Unit of Work を正しく関連付けるには、Inversion of Control コンテナが使用されます。

永続性の無視は、.NET コミュニティでは賛否両論あり、その重要性については盛んに議論されています。現時点では、永続性の無視は Microsoft .NET Framework 自体またはそれを拡張した .NET エコシステムで使用できる永続化ツールではあまりサポートされていません。NHibernate は最新のツールですが、それでも永続化を可能にするにはドメイン モデル側でなんらかの妥協を行う必要があります。

ご意見やご質問は、mmpatt@microsoft.com まで英語でお送りください。

Jeremy Miller は、C# の Microsoft MVP であり、.NET での依存関係の注入用に公開されているオープン ソースの StructureMap ツール、および .NET での FIT テスト用に近日公開予定の意欲的な StoryTeller ツールの作成者でもあります。彼のブログ「The Shade Tree Developer (日陰の開発者)」は、CodeBetter サイト上で公開されています。