データ ポイント

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

Julie Lerman

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

Julie Lerman今年、Eric Evans のソフトウェア設計に関する画期的な書籍『Domain-Driven Design: Tackling Complexity in the Heart of Software』(Addison-Wesley Professional、2003 年、amzn.to/ffL1k、英語) が刊行 10 周年を迎えました。この書籍に Evans は、長年にわたって大企業にソフトウェア構築プロセスを指導してきた経験を盛り込みました。Evans はその後さらに年月を費やして、クライアントとのやり取り、解決しようとしているビジネス課題の分析、チーム形成、ソフトウェア設計など、このようなソフトウェア構築プロジェクトを成功に導くパターンのカプセル化方法を検討しました。こうしたパターンはビジネスのドメインを重視しており、パターンが組み合わさってドメイン駆動設計 (DDD) を構成しています。DDD では、対象とするドメインをモデル化します。このようなドメインに関する開発者の知識を抽象化することで、パターンが生まれます。Martin Fowler の序文と Evans のまえがきを読み返すと、DDD の本質について、現在でも洞察に富んだ概要を把握できます。

このコラムでは、今回から 3 回にわたる連載として、私が DDD の技術パターンをコードに活用しようとした過程から、データを重視する Entity Framework の頭脳で DDD を理解するのに役立ったいくつかの指針を紹介していきます。

DDD が重要な理由

私が DDD について初めて知ったのは、.NET コミュニティなどで尊敬を集めるアーキテクト、Jimmy Nilsson に対する InfoQ.com (英語) での短いビデオ インタビュー (bit.ly/11DdZue、英語) でした。このインタビューで、Nilsson は LINQ to SQL と Entity Framework について語っていました。インタビューの最後に、Nilsson はお気に入りの技術書は何かと質問され、次のように答えました。「お気に入りのコンピューター書籍は Eric Evans の『Domain-Driven Design』です。あの本は、まるで詩のようです。内容が優れているだけではなく、詩を読むように気持ちよく何度も読み返すことができます」。詩とはうまい表現です。当時、私は技術関連の最初の拙著である『Programming Entity Framework』(O’Reilly Media、2009 年) の執筆中で、この表現に興味をそそられました。そこで、私は Evans の本がどのようなものか少し読んでみました。Evans の文章は、美しく流れるような文体です。この文体と、ソフトウェア開発に対する鋭い自然主義的な視点が相まって、楽しんで読める本にしあがっていました。同時に、書籍の内容にも驚きました。文章の書き方がすばらしいだけでなく、その内容も興味深いものでした。Evans はコードだけを単調に説明するのではなく、顧客との関係構築や、顧客の (問題となっているソフトウェアに関する) ビジネスとビジネス課題の正確な理解について論じていました。これは、私の 25 年にわたるソフトウェア開発を通じて重要だったことです。私はもっと知りたくなりました。

私は数年の間 DDD の本格的な学習をためらっていましたが、その後着手しました。カンファレンスで Evans に会い、彼の 4 日間の集中ワークショップに参加しました。私は決して DDD の専門家ではありませんが、自分のソフトウェア作成プロセスをより組織的で管理しやすい構造に移行しようとしていたところだったので、境界コンテキスト パターンはすぐに役立つパターンだと感じました。このトピックについては、2013 年 1 月号のこのコラム「DDD 境界コンテキストで EF モデルを縮小する」(msdn.microsoft.com/magazine/jj883952、英語) を参照してください。

それ以来、私はさらに勉強してきました。私は DDD に興味があってその刺激を受けていますが、DDD を成功に導く技術パターンのうちいくつについては、データ駆動の視点から理解することに苦労しています。多くの開発者も同じ苦労を経験していると思われるため、この連載では、Evans を筆頭とする多くの DDD の実践者や指導者 (Paul Rayner、Vaughn Vernon、Greg Young、Cesar de la Torre、Yves Reynhout など) のサポート、関心、および寛大さのおかげで私が習得できた教訓を紹介します。

ドメインをモデル化する際は永続性について考えない

ドメインのモデル化とは、ビジネスのタスクに注目することです。型とそのプロパティや動作を設計する際は、データベースで関係性がどのように機能するか、また選択したオブジェクト リレーショナル マッピング (ORM) フレームワーク (Entity Framework) で、構築中のプロパティ、関係性、および継承階層をどのように処理するか検討することに、強く興味をそそられます。データの保存と取得を業務とする企業向けのソフトウェア (Dropbox など) を構築していない限り、データの永続性はアプリケーションで補助的な役割しか果たしません。どちらかと言えば、現在の気温をユーザーに表示するために気象情報 API を呼び出す処理や、Meetup.com への登録のようにアプリケーションから外部サービスにデータを送信する処理に近い役割です。もちろん、データがもっと複雑な場合もありますが、境界コンテキストの DDD 手法を採用すれば、動作に注目しながら DDD の指針に従って型を構築することになるので、現在構築しているシステムよりも永続性を大幅に簡略化できます。

Entity Framework Fluent API を使ってデータベース マッピングを構成する方法を学習するなど、ORM について詳しく調べたことがあれば、必要に応じて永続性を利用できます。最悪の場合、クラスを調整する必要が生じることがあります。また、レガシ データベースなどの特殊な場合には、データベース マッピング用に設計された永続性モデルを追加してから、AutoMapper などを使用してドメイン モデルと永続性モデルの関係を解決することもできます。

しかし、これらの懸念事項は、ソフトウェアの開発によって解決を目指すビジネス課題とは関係がないので、永続性はドメイン設計に影響しません。これが私にとって問題になるのは、自分でエンティティを設計しており、エンティティのデータベース マッピングが EF によって推測される方法を検討せずにはいられないためです。そのため、このような不要な考慮事項の無視に努めています。

プライベート セッターとパブリック メソッド

もう 1 つの教訓は、プロパティ セッターをプライベートにすることです。呼び出し元コードでさまざまなプロパティを不規則に設定できるようにするのではなく、プロパティを変更するメソッドを使用して、DDD オブジェクトとその関連データの操作を管理する必要があります。ここでの "プロパティを変更するメソッド" とは、SetFirstName や SetLastName などのメソッドではありません。たとえば、新しい Customer 型のインスタンスを作成して各インスタンスのプロパティを設定する代わりに、新しい顧客 (Customer) の作成時に検討する規則を定めておく場合を指しています。これらの規則を Customer のコンストラクターに組み込んで、ファクトリ パターン メソッドを使用したり Customer 型の Create メソッドを配置したりすることができます。図 1 は、集計ルート (DDD でのオブジェクト グラフの "親" で、別名は "ルート エンティティ") の DDD パターンに従って定義した Customer 型を示しています。Customer 型のプロパティにはプライベート セッターがあるため、これらのプロパティに直接影響するのは、同じ Customer クラスに含まれる他のメンバーだけです。クラスでは、コンストラクターを 1 つ公開してインスタンスの作成方法を制御し、(Entity Framework に必要な) パラメーターのないコンストラクターを内部コンストラクターとして非公開にします。

図 1 集計ルートとして機能する型のプロパティとメソッド

public class Customer : Contact
{
  public Customer(string firstName,string lastName, string email)
  { ... }
  internal Customer(){ ... }
  public void CopyBillingAddressToShippingAddress(){ ... }    
  public void CreateNewShippingAddress(
   string street, string city, string zip) { ... }
  public void CreateBillingInformation(
   string street, string city, string zip,
   string creditcardNumber, string bankName){ ... }    
  public void SetCustomerContactDetails(
   string email, string phone, string 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 型は、一部の住所やクレジット カードの種類など、集計の他のエンティティを制御して保護するために、そのようなオブジェクトを作成して操作する特定のメソッド (CopyBillingAddressToShippingAddress など) を公開します。集計ルートでは、このようなメソッドに実装したドメインのロジックと動作を使用して、集計対象の各エンティティを定義する規則を適用する必要があります。最も重要なのは、集計ルートによって集計全体のインバリアント ロジックと一貫性が管理されていることです。インバリアントについては次回のコラムで詳しく説明しますが、差し当たっては、Jimmy Bogard による「Strengthening Your Domain: Aggregate Construction」(ドメインを強化する: 集計構築、英語) という集計におけるインバリアントのすばらしい説明を記したブログ記事 (bit.ly/ewNZ52) をご覧になることをお勧めします。

最終的に、Customer 型が公開するのはプロパティではなく動作 (CopyBillingAddressToShippingAddress、CreateNewShippingAddress、CreateBillingInformation、および SetCustomerContactDetails) です。

Customer 型の派生元である Contact 型は、他のクラスで必要になる場合があるため、Common という名前の別のアセンブリに存在していることに注意してください。Contact 型のプロパティは非公開にする必要がありますが、Customer 型からアクセスできなくなるためプライベートには設定できません。代わりに、スコープを Protected に指定します。

public class Contact: Identity
{
  public string CompanyName { get; protected set; }
  public string EmailAddress { get; protected set; }
  public string Phone { get; protected set; }
}

Identity 型について補足すると、Customer と Contact はキー値がないので DDD の値オブジェクトのように見えることでしょう。しかし、このソリューションではキー値は Contact の派生元の Identity クラスから提供されています。また、Customer 型も Contact 型も不変ではないので、値オブジェクトと見なすことはできません。

Customer 型は Contact 型を継承しているため、次の SetCustomerContactDetails メソッドのように、Contact 型の保護されたプロパティにアクセスでき、値を設定できます。

public void SetCustomerContactDetails  (string email, string phone, string companyName)

{

  EmailAddress = email;

  Phone = phone;

  CompanyName = companyName;

}

CRUD だけで十分な場合がある

アプリには、DDD を使用しなくても作成できる部分があります。DDD が役に立つのは、複雑な動作を処理する場合です。直接的で不規則な編集やクエリだけが必要な場合は、シンプルなクラス (のセット) をEF の Code First における通常の (プロパティと関係性を使用した) 定義と同様に定義して、(リポジトリや単に DbContext を使用して) 挿入、更新、および削除の各メソッドと組み合わせていれば十分です。そのため、注文と注文品目の作成などのタスクを実行するには、特別なビジネス規則と動作で DDD を作業の役に立てる必要があります。たとえば、注文者が得意客かどうかを判断する必要があるとします。この場合、顧客の詳細情報をいくらか取得して判断を下し、得意客の場合は注文に含まれている各品目に 10% の割引を適用する必要があります。ユーザーがクレジット カード情報を指定している場合は、カードが有効なことを確かめるために検証サービスを呼び出す必要もあります。

DDD で重要なのは、ドメイン ロジックをドメインのエンティティ クラス内のメソッドとして含め、OOP を活用することです。多くのデモウェアの Code First クラスのように "トランザクション スクリプト" をステートレスなビジネス オブジェクト内に実装することはありません。

ただし、連絡先レコード (名前、住所、参照元など) の作成と保存など、ごく標準的な処理を実行するだけの場合もあります。つまり、作成、読み取り、更新、削除 (CRUD) を行う場合を指します。この場合、集計とルートを作成して要件を満たすように動作を作成する必要はありません。

多くの場合、アプリケーションには複雑な動作と単純な CRUD の組み合わせが含まれています。そのため動作を明確にすることに時間を割いて、実際には単純なアプリケーションの構成要素の過剰設計で時間、労力、および経費を無駄にしないようにします。この場合、異なるサブシステムや境界コンテキストの間の境界を特定することが重要です。境界コンテキストがまさにデータ駆動 (CRUD) の場合もありますが、重要なコア ドメイン境界コンテキストは DDD の手法に従って設計する必要があります。

共有データは複雑なシステムで障害になることがある

私が壁にぶつかり、親切な人が詳しく説明しようとしてくれた際に不満や泣き言をこぼした問題には、サブシステム間での型とデータの共有に関する問題もあります。両方を同時には共有できないことに気付いたため、"必ずシステム間で型を共有してすべての共有型で同じデータベースの同じテーブルを操作する必要がある" という前提を考え直すことになりました。

現在では、データの共有が必要な箇所について、慎重に検討してから判断するよう努力しています。さまざまなコンテキストから単一テーブルへのマッピング、さらには単一データベースへのマッピングなど、試す価値のない箇所もあります。最も一般的な例は、複数のシステム全体のニーズを満たそうとする Contact の共有です。さまざまなシステムで必要になる可能性がある Contact 型のソース管理を調整して活用するには、どうすればよいでしょうか。あるシステムで Contact 型の定義を変更する必要がある場合はどうでしょうか。ORM に関して、複数のシステムにまたがって使用する Contact 型を単一データベースの単一テーブルにマッピングするには、どうすればよいでしょうか。

DDD を利用すれば、単一データベースの同一人物に関するテーブルを必ずしも参照する必要がないことがわかりるので、ドメイン モデルとデータの共有から解放されます。

私がこの考えを受け入れるうえで最大の障害となったのは、25 年間もの間、再利用 (コードの再利用とデータの再利用) の利点を重視していたことです。そのため、データの複製は不適切な処理ではないという考え方には手を焼いていますが、少しずつ慣れてきています。もちろん、すべてのデータがこの (私にとって) 新しいパラダイムに適しているわけではありません。しかし、人名のような簡単なデータならどうでしょうか。たとえば、ソフトウェア ソリューションのさまざまなサブシステム専用の、複数のテーブルや複数のデータベースで名と姓を複製する場合はどうでしょうか。長期的には、データ共有の複雑さを軽減すればシステム構築作業を大幅に簡略化できます。いずれにしても、常にデータを最小限に抑え、別の境界コンテキストにデータの複製を配置する必要があります。たとえば、Pricing の境界コンテキストで、割引を計算するために顧客の ID とステータスが必要になる場合に、この同じ顧客の名と姓は Contact Management の境界コンテキストだけで必要になるときがあります。

ただし、システム間で共有が必要な情報はまだ大量にあります。DDD で (サービスやメッセージ キューのように単純なこともある) "腐敗防止層" と呼ばれるものを利用すると、たとえば、あるシステムでユーザーが新しい連絡先を作成した場合に、その連絡先が既に他の場所に存在することを検出したり、その連絡先を共通の ID キーで他のサブシステムに作成したりできます。

来月までの検討事項

私はドメイン駆動設計の技術面を学習して理解し、これまでの習慣を新しいアイデアに合わせて調整しようと苦心し、ついに納得の瞬間を迎える過程を何度も経験してきたので、今回紹介した指針は物事を前向きに考えるうえで実に効果的でした。一部の課題は視点の問題にすぎないこともあるため、今回の指針は、物事を理解しやすくする視点を交えて説明しています。

次回のコラムでは、その他の納得の瞬間の紹介として、聞き覚えがある方もいらっしゃるでしょうが、"ドメイン モデル貧血症" とその DDD 版の "リッチ ドメイン モデル" という用語について説明します。また、一方向の関係性と、Entity Framework の使用時にデータの永続化を取り入れるタイミングについても説明します。さらに、読者の皆さんが短期間で習得できるよう、私が非常に苦労したその他の DDD のトピックについても取り上げます。

それまでは、ぜひ手元のクラスを詳しく調べ、クラスがより管理しやすくなる方法を検討し、クラスのプロパティ セッターを非公開にして、より説明的で明示的なメソッドを公開してください。また、"SetLastName" メソッドは使用しないでください。それは反則です。

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 のコースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Cesar de la Torre (マイクロソフト) に心より感謝いたします。