データ ポイント

ドメイン駆動設計のコーディング: データを重視する開発者のためのヒント (第 2 部)

Julie Lerman

コード サンプルのダウンロード

今月のコラムでも、引き続き Eric Evans の書籍『エリック・エヴァンスのドメイン駆動設計』(翔泳社、2011 年) の刊行 10 周年を記念して、データを最優先に考え、ドメイン駆動設計 (DDD) のコーディング パターンによる効果に関心がある開発者のために、新たなヒントを紹介します。先月のコラムの要点は以下のとおりです。

  • ドメインをモデル化する際は永続性について考えない
  • エンティティや集計を操作するメソッドは公開し、プロパティ セッターは公開しない
  • 一部のサブシステムは単純な作成、読み取り、更新、および削除 (CRUD) 操作に最適で、ドメインをモデル化する必要もないことを認識する
  • データや型を境界コンテキストを超えて共有しないことにはメリットがある

このコラムでは、"ドメイン モデル貧血症" と "リッチ ドメイン モデル" という用語が表す意味と、値オブジェクトの概念について学習します。値オブジェクトは、開発者の間で感想が真二つに分かれるトピックのようです。説明の必要性をまったく感じないほどわかりやすいと感じる開発者もいれば、理解しようとすることもできないほど不可解だと感じる開発者もいます。また、特定のシナリオでは関連オブジェクトではなく値オブジェクトの使用をお勧めする理由についても説明します。

見下すような用語: ドメイン モデル貧血症

DDD におけるクラスの定義方法に関して、頻繁に耳にする用語が 2 つあります。それはドメイン モデル貧血症とリッチ ドメイン モデルです。DDD では、ドメイン モデルとはクラスを意味します。リッチ ドメイン モデルは、DDD 手法に沿ったドメイン モデルで、ゲッターやセッターだけでなく動作で定義したクラス (型) です。一方、ドメイン モデル貧血症はゲッターとセッターのみで構成されています (簡単なメソッドをいくつか含んでいる場合もあります)。そのため、多くのシナリオでは問題なく動作しますが、DDD によるメリットは得られません。

私が Entity Framework (EF) の Code First を使い始めたとき、Code First と連携するように定義したクラスのおそらく 99% は貧血症のクラスでした。図 1は、Customer 型とその継承元である Person 型を示した良い例です。多くの場合はさらにメソッドをいくつか追加しますが、基本の型はゲッターとセッターを備えた単なるスキーマでした。

図 1 データベース テーブルに似た一般的なドメイン モデル貧血症のクラス

public class Customer : Person
 {
   public Customer()
   {
     Orders = new List();
   }
   public ICollection Orders { get; set; }
   public string SalesPersonId { get; set; }
   public ShippingAddress ShippingAddress { get; set; }
 }
 public abstract class Person
 {
   public int Id { get; set; }
   public string Title { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string CompanyName { get; set; }
   public string EmailAddress { get; set; }
   public string Phone { get; set; }
 }

私は、ようやくこのようなクラスを見て、私の行動がクラスでデータベース テーブルを定義するのと大差ないことに気付きました。しかし、型の用途によっては、これは必ずしも問題になるとは限りません。

この貧血症の型を、図 2に示すより機能豊富な Customer 型と比較してみましょう。この Customer 型は、先月のコラム (msdn.microsoft.com/magazine/dn342868) で説明した Customer 型に似ています。機能豊富な Customer 型では、集計の一部であるプロパティや他の型へのアクセスを制御するメソッドを公開しています。今月号で説明するトピックをわかりやすく表すために、Customer 型は先月のコラムから少し変更しています。

図 2 プロパティだけではないリッチ ドメイン モデルの Customer 型

public class Customer : Contact
 {
   public Customer(string firstName, string lastName, string email)
   {
     FullName = new FullName(firstName, lastName);
     EmailAddress = email;
     Status = CustomerStatus.Silver;
   }
   internal Customer()
   {
   }
   public void UseBillingAddressForShippingAddress()
   {
     ShippingAddress = new Address(
       BillingAddress.Street1, BillingAddress.Street2,
       BillingAddress.City, BillingAddress.Region,
       BillingAddress.Country, BillingAddress.PostalCode);
   }
   public void CreateNewShippingAddress(string street1, string street2,
    string city, string region, string country, string postalCode)
   {
     BillingAddress = new Address(
       street1,street2,
       city,region,
       country,postalCode)
   }
   public void CreateBillingInformation(string street1,string street2,
    string city,string region,string country, string postalCode,
    string creditcardNumber, string bankName)
   {
     BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
     CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
   }
   public void SetCustomerContactDetails
    (string email, string phone, string companyName)
   {
     EmailAddress = email;
     Phone = phone;
     CompanyName = companyName;
   }
   public string SalesPersonId { get; private set; }
   public CustomerStatus Status { get; private set; }
   public Address ShippingAddress { get; private set; }
   public Address BillingAddress { get; private set; }
   public CustomerCreditCard CreditCard { get; private set; }
 }

機能豊富なこのモデルでは、読み取りや書き込み対象のプロパティを公開しているだけではなく、外部メソッドで Customer のパブリック サーフェイスを形成しています。詳細については、先月のコラムの「プライベート セッターとパブリック メソッド」セクションを参照してください。この例の目的は、DDD でドメイン モデル貧血症と呼ぶものとリッチ ドメイン モデルと呼ぶものの違いを理解しやすくすることです。

値オブジェクトは混乱を招くことがある

DDD の値オブジェクトは、単純なように思えますが、(私を含む) 多くの開発者がひどく混乱するトピックです。値オブジェクトについては、さまざまな観点からさまざまな方法で述べた説明を読んだり聞いたりしてきました。しかしさいわいにも、それらの各説明が他の説明と食い違うことはなく、値オブジェクトに関する理解を深めるのに役立ちました。

値オブジェクトの基本は、ID キーのないクラスです。

Entity Framework には、かなり近い意味を持つ "複合型" という概念があります。複合型は、プロパティのセットをカプセル化する手段なので、ID キーは備わっていません。EF には、データの永続化に際してこうした値オブジェクトを処理する方法が用意されています。値オブジェクトは、エンティティのマッピング先となるテーブルのフィールドとしてデータベースに格納されます。

たとえば、Person クラスの FirstName プロパティや LastName プロパティの代わりに、FullName プロパティを使用できます。

public FullName FullName { get; set; }

FullName プロパティは、FirstName プロパティと LastName プロパティを渡すコンストラクターを使用して、2 つのプロパティをカプセル化するクラスにできます。

ただし、値オブジェクトには複合型よりも豊富な機能が用意されています。DDD では、値オブジェクトに関して 3 つの明確な特徴があります。

  1. ID キーがない (複合型と同様)
  2. 不変である
  3. 同じ型の他のインスタンスに対する等価性を確認する場合、値オブジェクトのすべての値を比較する

興味深いのは、不変性を備えていることです。ID キーがないため、型の識別情報は型の不変性によって決まります。必ずプロパティの組み合わせでインスタンスを識別できます。不変性があることから型のプロパティが変更されないため、特定のインスタンスの識別に型の値を使用しても問題ありません (DDD のエンティティではセッターをプライベートにすることでエンティティの不規則な変更を防いでいることを前回紹介しましたが、プライベート プロパティの変更許可をメソッドに与えることもできます)。値オブジェクトでは、外部からセッターにアクセスできないだけでなく、開発者がプロパティを変更することもできません。プロパティを変更した場合は、値オブジェクトの値が変化したことになります。値オブジェクトの値はオブジェクト全体で表すため、プロパティを個別に操作することはありません。オブジェクトに複数の値を設定する必要がある場合は、新しい値のセットを格納するインスタンスを新規作成します。つまり、値オブジェクトのプロパティの変更処理は存在しません。したがって、"プロパティの値を変更する必要がある場合" というフレーズが含まれた文章を作ることも事実上矛盾しています。他に役に立つ類似概念として、(少なくとも私の知っているすべての言語では) もう 1 つの不変な型である string について考えることが挙げられます。 string インスタンスを操作する場合は、その string に含まれている個別の文字を置き換えることはなく、単純に新しい string を作成します。

図 3 に、FullName 値オブジェクトを示します。この値オブジェクトには ID キー プロパティがありません。そのため、この値オブジェクトのインスタンスを作成する方法は 2 つのプロパティ値を設定する以外になく、いずれのプロパティも変更できないことがわかります。したがって、この値オブジェクトは不変性に関する要件を満たしています。同じ型の別のインスタンスと等価性を比較する手段を提供するという最後の条件は、ValueObject カスタム クラス (bit.ly/13SWd9h(英語) で Jimmy Bogard から借用) に隠されています。このクラスには多数の複雑なコードがあるため、FullName 値オブジェクトではこのクラスを継承しています。この ValueObject ではコレクション プロパティが考慮されていませんが、この値オブジェクトにはコレクションが含まれていないため、ここでのニーズを満たしています。

図 3 FullName 値オブジェクト

public class FullName:ValueObject
 {
   public FullName(string firstName, string lastName)
   {
     FirstName = firstName;
     LastName = lastName;
   }
   public FullName(FullName fullName)
     : this(fullName.FirstName, fullName.LastName)
   {    }
   internal FullName() { }
   public string FirstName { get; private set; }
   public string LastName { get; private set; }
  // More methods, properties for display formatting
 }

『Implementing Domain-Driven Design』(Addison-Wesley Professional、2013 年) の著者である Vaughn Vernon は、変更したコピー (この例の FullName によく似ているが FirstName が異なるなど) が必要になる場合があることを考慮して、FullName には既存のインスタンスから新しいインスタンスを作成するメソッドを含められるとしています。

public FullName WithChangedFirstName(string firstName)
 {
   return new FullName(firstName, this.LastName);
 }
 public FullName WithChangedLastName(string lastName)
 {
   return new FullName(this.FirstName, lastName);
 }

永続化層 (Entity Framework) に追加すると、この値オブジェクトは、使用先となるあらゆるクラスの EF ComplexType プロパティとして認識されます。FullName 値オブジェクトをを Person 型のプロパティとして使用すると、EF により、Person 型を格納するデータベース テーブルに FullName のプロパティが格納されます。データベースでは、これらのプロパティに既定で FullName_FirstName と FullName_LastName という名前が付けられます。

FullName はかなり単純なので、データベース中心の設計でも、FirstName と LastName を別個のテーブルに格納しない場合があります。

値オブジェクトと関連オブジェクト

ここで、別のシナリオを考えてみましょう。ShippingAddress と BillingAddress を設定できる Customer 型を想像してください。

public Address ShippingAddress { get; private set; }
 public Address BillingAddress { get; private set; }

(データ駆動の頭脳にとっての) 既定では、Address は、AddressId プロパティも含めてエンティティとして作成します。そしてここでも "データ中心に考えている" ので、データベースで Address が独立したテーブルとして格納されると想定しています。いえ、私はドメインをモデル化するときは、データベースについて考えないように努めているので、このことは重要ではありません。ただし、EF を使用してデータ層に追加する時点でも、同じように想定しています。しかし、EF ではマッピングが正しく行われません。Address と Customer の間では 0..1:* のリレーションシップが想定されています。つまり、1 つの Address には関連する Customer がいくつでも存在でき、1 つの Customer には 0 または 1 つの Address が存在できます。

この結果を DBA は気に入らないかもしれません。もっと重要なことに、アプリケーション コードの記述は、0 または 1 つの Address に対して多数の Customer があるという Entity Framework と同じ想定に基づいているわけではありません。したがって、予期しない形で EF がデータの永続性と取得に影響を及ぼす可能性があります。そこで、EF をかなり多用してきた者としての最初の手法は、Fluent API を使用して EF マッピングを修正することです。しかし、EF でこの問題を解決するには、Address から Customer を再度参照するナビゲーション プロパティを追加することになります。これは、私のモデルでは望ましくありません。このように、EF のマッピングでこの問題を解決しようとすると、困難な状況が待ち受けています。

>一歩離れて EF でもデータベースでもなくドメインに注目してみると、Address はエンティティではなく値オブジェクトにする方が合理的だとわかります。これには次の 3 つの手順を踏む必要があります。

  1. Address 型からキー プロパティ (おそらく Id または AddressId という名前) を削除する。
  2. Address を不変にする: Address のコンストラクターを使用することで、既にすべてのフィールドに値を設定できています。プロパティを変更できるメソッドを、すべて削除する必要があります。
  3. Address のプロパティやフィールドに基づいて Address の等価性を確認できるようにする: これは、Jimmy Bogard から借用した優れた ValueObject クラスを再度継承することで実現できます (図 4 参照)。

図 4 値オブジェクトとしての Address

public class Address:ValueObject

{ public Address(string street1, string street2, string city, string region, string country, string postalCode) { Street1 = street1; Street2 = street2; City = city; Region = region; Country = country; PostalCode = postalCode; } internal Address() { } public string Street1 { get; private set; } public string Street2 { get; private set; } public string City { get; private set; } public string Region { get; private set; } public string Country { get; private set; } public string PostalCode { get; private set; } // ... }

Customer クラスからこの Address を使用する方法に、変化はありません (例外として、Address を変更する場合は、新しいインスタンスを作成する必要があります)。

しかし、これでリレーションシップの管理について心配する必要がなくなりました。また、これは、Customer を実際に発送先情報や請求情報によって定義するという点で、値オブジェクトに関するもう 1 つのバロメーターになります。これは、私がその顧客に何かを販売する場合、おそらく私はその商品を発送する必要があり、買掛金部門からその商品の請求先住所を把握するよう要求されるためです。

これは 1:1 または 1:0..1 のすべてのリレーションシップを値オブジェクトに置き換えられるということではありませんが、このシナリオでは優れた解決策になります。Entity Framework でこのリレーションシップを保持するにあたって次々に現れていた難題を解決する必要がなくなった結果、作業が大幅に単純化されました。

FullName の例のように、Address を値オブジェクトに変更すると、Entity Framework では Address が複合型として認識され、すべてのデータが Customer テーブルに格納されるようになります。長年データベースの正規化に重点を置いていた経験から、私は反射的にこれは問題だと考えましたが、この現象は手元のデータベースで容易に対応でき、私のドメインでは有効に機能しています。

この手法の使用に対する反論は多数思い付きますが、すべての反論の先頭には "もし" という言葉が付いています。私のドメインに関係しない限り、どれも有効な反論ではありません。開発者は、起こることのない万が一のシナリオに備えたコーディングで、多くの時間を無駄にしています。このような事前の応急処置をソリューションに追加することに関して、私はもっと慎重になろうと努めています。

まだ終わりではありません

このデータおたくの頭に思い浮かんだ一連の DDD 概念の説明も、もうすぐ終わりです。これらの概念は、知れば知るほど学びたくなります。考え方を少し変えれば、これらのパターンは非常に理にかなっています。これらのパターンを学んでもデータの永続性に対する興味はまったく薄れていませんが、ドメインをデータの永続性やデータ インフラストラクチャから切り離して考えることは、長年それらの概念で混乱してきた後では適切に感じられます。ただし、DDD が不要なソフトウェア関連活動も多いことに留意してください。DDD は、複雑な問題の解決には便利ですが、単純な問題には大げさすぎることが多々あります。

次回のコラムでは、データ優先の考え方に一見矛盾するように思える DDD の他の技術的戦略 (双方向のリレーションシップを排除する、インバリアントについて考慮する、集計からデータ アクセスを開始するニーズに気付いた場合に対処するなど) について、いくつか説明します。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog(英語) で、『Programming Entity Framework』(O'Reilly Media、2010 年)、および同書の Code First 版 (O'Reilly Media、2011 年) と DbContext 版 (O'Reilly Media、2012 年) の著者でもあります。Twitter (twitter.com/julielerman、英語) で彼女をフォローし、juliel.me/PS-Videos(英語) で Pluralsight のコースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Stephen Bohlen (マイクロソフト) に心より感謝いたします。
Stephen A. Bohlen は、現在マイクロソフトのシニア テクノロジ エバンジェリストとして活躍しています。彼は、アーキテクト、CAD マネージャー、IT 技術者、ソフトウェア エンジニア、CTO、およびコンサルタントとしての 20 年以上にわたる多彩な経験を生かし、最先端やプレリリース版の開発者向けマイクロソフト製品およびマイクロソフト テクノロジの導入において、マイクロソフト パートナー組織の選択をサポートしています。