ドメイン設計入門
本書は、ドメイン駆動設計(Test-Driven Development)について書かれている。
ドメイン駆動設計とは
利用者にとって役立つソフトウェアを開発するためには価値ある知識と無価値な知識を慎重に切り分け、価値ある知識をコードに落とし込む必要がある。物流システムを開発する場合、「トラックの積載容量や燃料などの概念」は利用価値の高い知識である。逆に「トラックの語源がラテン語のtrochusである」といった知識は物流システムにとってはほとんど無価値である。このように利用価値の高い知識を選び分けるには、ソフトウェアの利用者を取り巻く世界について知る必要がある。価値あるソフトウェアを構築するためには利用者の問題を見極め、解決するための最善手を常に考える。ドメイン駆動設計とはそういった洞察を繰り返しながら設計を行い、ソフトウェアの利用者を取り巻く世界と実装を結びつけることを目的としている。
ドメインとは
「領域」の意味を持った言葉である。ソフトウェア開発におけるドメインとは「プログラムを適用する対象となる領域」を指す。例えば、物流システムであれば貨物や倉庫といった概念がドメインに含まれる。
ソフトウェアの目的は利用者のドメインにおける何らかの問題解決である。利用者が直面している問題を解決するためには、「利用者が直面している問題」を正確に理解する必要がある。つまりドメインと向き合う必要がある。ドメインの概念や事象を理解し、その中から問題解決に役立つ価値のある知識をソフトウェアに反映する。技術的なアプローチで解決を図ろうとするのではなく、ドメインと向き合い、そこに渦巻く知識に焦点を当てる。
ドメインモデルとは
モデルとは現実の事象、概念を抽象した概念である。現実をすべて忠実に再現するのではなく、必要に応じて取捨選択を行う。何を取捨選択するかはドメインによる。例えば、小説家にとってペンは道具で、文字が書けることこそが大切である。一方、文房具店にとってペンは商品である。文字が書けることよりもその値段などが重要視される。つまり、対象が同じものであっても何に重きを置くかは異なるということである。
事象、概念を抽象化する作業がモデリングと呼ばれる。その結果として得られるのがモデルである。ドメインの概念をモデリングして得られたモデルのことをドメインモデルと呼ぶ。ドメインの問題を解決するソフトウェアを構築するためには、ドメインの知識のある人とソフトウェアの知識がある人が協力してドメインモデルを作り上げる必要がある。
ドメインオブジェクトとは
ドメインモデルは概念を抽象化した知識にとどまる。問題解決するためには、ドメインモデルを何かしらの媒体で表現する必要がある。
ドメインモデルをソフトウェアで動作するモジュールとして表現したものがドメインオブジェクトである。
ドメインはときの流れとともに変化する。ドメインで起こった変化はまずドメインモデルに伝えられる。ドメインオブジェクトはドメインモデルの実装表現なので、変化したドメインモデルと変化していないドメインオブジェクトの両者を見比べると修正箇所が明らかになる。つまり、ドメインの変化はドメインモデルを媒介にして連鎖的にドメインオブジェクトまで伝えられる。
知識を表現するためのパターン(ドメインオブジェクト)
値オブジェクト
プリミティブな値ではなく、システム固有の値を表現するために定義されたオブジェクトが値オブジェクトである。
例えば、以下のように氏名を出力するコードがあるとする。
var fullName = "toya masuda";
Console.WriteLine(fullName);
fullNameのシステムにおける取り扱い方はさまざまである。氏名をフルネームで使いたい場合、姓だけを表示させたい場合などがある。
この場合、以下のようにオブジェクトにすることで解決することができる。
class FullName
{
public FullName(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; }
public string LastName { get; }
システムには必要とされる処理に従って、そのシステムならではの値の表現がある。
オブジェクトでもあり、値でもある。それが値オブジェクトである。
値オブジェクトの性質
値がもつ性質は値オブジェクトにそのまま適用される。
- 不変である
- 交換が可能である
- 等価性によって比較される
値オブジェクトにする基準
判断基準は「そこにルールが存在しているか」と「それ単体で取り扱いたいか」という点が重要視される。
例えば、氏名であれば「姓と名で構成される」というルールがある。そのため、氏名を単体で扱いたい場合は値オブジェクトとして実装した方が良い。
エンティティ
エンティティは値オブジェクトと同様にドメインモデルを実装したドメインオブジェクトである。値オブジェクトとの違いは同一性によって識別されるか否かである。つまり、エンティティとは属性で区別されないオブジェクトである。例えば、システムのユーザという概念はその典型である。システムの利用者は最初にユーザ登録をし、利用者個人の情報をユーザ情報として登録する。ユーザ情報はたいていの場合任意で変更可能である。このとき、ユーザ情報として登録されているデータを変更しても、ユーザ自体が変更されたわけではないため別のユーザになることはない。ユーザは属性ではなく同一性により識別されているからである。
姓と名の属性からなる氏名であれば、いずれかの属性が変更されてしまうとまったく異なるものになってしまう。反対に属性がまったく同じであった場合は同じものであるとみなされる。このようにその属性によって識別されるオブジェクトは値オブジェクトである。
エンティティの性質
- 可変である
エンティティの属性は変化することが許容されている。値オブジェクトは不変の性質が存在するため交換(代入)によって変更を表現していた。しかし、エンティティは属性を変化させたい場合は、交換ではなくふるまいを通じて変更することになる。
class User
{
private string name;
public User(string name)
{
ChangeName(name);
}
public void ChangeName(string name)
{
this.name = name;
}
}
- 同じ属性であっても区別される
- 同一性により区別される
同一性を判断するためには何らかの手段が必要である。プログラムでは同一性の判断を実現するために識別子を利用する。
private readonly UserId id; // 識別子
readonly修飾子を付けて再代入を不可にし、インスタンスが実体化している間もIDが変化しないことを保証する。
エンティティの比較処理には同一性を表す識別子だけが比較の対象となる。
ドメインサービス
ドメイン駆動設計で取りざたされるサービスは大きく2つある。一つはドメインのためのサービスで、もう一つがアプリケーションのためのサービスである。前者をドメインサービスと呼び、後者をアプリケーションサービスと呼ぶ。
システムには値オブジェクトやエンティティに記述すると不自然になってしまうふるまいが存在する。ドメインサービスはこの不自然さを解消するオブジェクトである。
不自然なふるまいとは
現実において同姓同名は起こりえるが、システムにおいてはユーザ名の重複を許可しないことがある。ユーザ名の重複を許さないのはドメインのルールなのでドメインオブジェクトのふるまいとして定義すべきである。
class User
{
private readonly UserId id;
private UserName name;
public User(UserId id, UserName name)
{
this.id = id;
this.name = name;
}
public book Exists(User user)
{
// 重複を確認するコード
}
}
実際に重複を確認するコードを呼び出す場合
var user = new User(userId, userName);
var duplicateCheckResult = user.Exists(user);
と重複の有無を自身に対して問い合わせることになる。これは多くの開発者を混乱させてしまう不自然なコードである。
このような不自然さを解決するためにドメインサービスを使用する。
class UserService
{
public book Exists(User user)
{
// 重複を確認するコード
}
}
これにより、自然にExistsを呼び出すことができる。
var userService = new UserService();
var user = new User(userId, userName);
var duplicateCheckResult = UserService.Exists(user);
このように値オブジェクトやエンティティに定義すると不自然に感じる操作はドメインサービスに定義する。
知識を表現する発展的なパターン
集約
データを変更するための単位として扱われるオブジェクトの集まりを集約という。集約にはルートとなるオブジェクトが存在し、すべての操作はルート越しに行われる。このように集約内部への操作に制限がかけられ、集約内の不変条件は維持される。
エンティティで記述したUserは集約にあたる。
集約は外部から境界の内部のオブジェクトを操作してはいけない。集約を操作するための直接のインターフェースとなるオブジェクトは集約ルートと呼ばれるオブジェクトに限定される。集約内部のオブジェクトに対する変更は集約ルートが責任をもって行う。
var userName = new UserName("NewName");
// NG
user.Name = userName;
// OK(集約ルート)
user.ChangeName(userName);
仕様
オブジェクトの評価は単純なものであればメソッドとして定義される。複雑な評価の手順はアプリケーションサービスに記述されてしまうことが多い。しかし、オブジェクトの評価はドメインの重要なルールなのでサービスに記述することは問題である。この対策として仕様がある。
仕様はあるオブジェクトがある評価基準に達しているかを判定するオブジェクトである。
以下のようなサークルを表すオブジェクトに評価を行うメソッドがあるとする。これほど単純な条件であればメソッドして定義した方が良い。
public class Circle
{
public bool IsFull()
{
return CountMembers() >= 30;
}
}
しかし、サークルの人数上限が所属しているユーザのタイプにより変動する場合は複雑になる。
- ユーザにはプレミアムユーザと呼ばれるタイプが存在する
- サークルに所属するユーザの最大数はサークルのオーナーとなるユーザを含めて30名まで
- プレミアムユーザが10名以上所属しているサークルはメンバーの最大数が50名に引き上げられる
こういった場合は、サークルが満員かどうかを評価する処理を仕様として切り出す。
pubilc class CircleFullSpecification
{
private readonly IUserRepository userRepository;
public CircleFullSpecification(IUserRepository userTepository)
{
this.userRepository = userRepository;
}
public bool IsSatisfiedBy(Circle circle)
{
var users = userRepository.Find(circle.Members);
var premiumUserNumber = users.Count(user => user.IsPremium);
var circleUpperLimit = premiumUserNumber < 10 ? 30 : 50;
return circle.CountMembers() >= circleUpperLimit;
}
}
アプリケーションを表現するためのパターン
リポジトリ
ソフトウェア開発の文脈で登場するリポジトリの意味合いはデータの保管庫である。
ソフトウェアにドメインの概念を表現しただけでは、アプリケーションとして成り立たせることが難しい。プログラムが実行される過程でメモリ上に展開されたデータはプログラムが終了すると消えてなくなる。
オブジェクトを繰り返し利用するには、何らかのデータストアにオブジェクトのデータを永続化(保存)し再構築(復元)する必要がある。リポジトリはデータを永続化し再構築するといった処理を抽象的に扱うためのオブジェクトである。データの永続化と再構築を直接データストアにアクセスするのではなく、リポジトリを経由することで柔軟性を与える。例えば、開発初期にどのデータストアを採用するのかが決まっていない場合でも、インメモリのリポジトリを利用してロジックを実装できる。
アプリケーションサービス
アプリケーションサービスとは、ユースケースを実現するオブジェクトである。例えば、ユーザ機能を実現するためには「ユーザ情報の登録」「ユーザ情報の変更」といったユースケースが必要である。アプリケーションサービスはユースケースにしたがって「ユーザ情報の登録」「ユーザ情報の変更」といったふるまいが定義される。これらのふるまいは、ドメインオブジェクトを組み合わせて実装する。つまり、ドメインの知識を表現するドメインオブジェクトの力をまとめあげドメインの問題を解決する。
ドメインオブジェクトだけではアプリケーションとして完成しない。アプリケーションサービスはドメインオブジェクトの操作に徹することでユースケースを実現する。
ファクトリ
複雑なオブジェクトはその生産過程も複雑な処理になることが多い。そうした処理はモデルを表現するドメインオブジェクトの趣旨をぼやけさせてしまう。求められるのは、複雑なオブジェクトの生成処理をオブジェクトとして定義することである。この生成を債務とするオブジェクトをファクトリという。ファクトリはオブジェクトの生成に関わる知識がまとめられたオブジェクトである。
採番処理の例:
識別子の採番処理をコントロールするために、シーケンスや採番テーブルを利用する。エンティティ内で実装しようとした場合、エンティティにデータベースの操作を記述しなければならない。そうなると、そのエンティティをインスタンス化するだけでもデータベースや採番テーブルの準備が必要になる。そのため、ドメインオブジェクトを生成する処理はファクトリに記述する。
public interfacce IUserFactory
{
User Create(UserName name);
}
public class UserFactory : IUserFactory
{
public User CreateUserName name)
{
string seqId;
// データベースの接続設定からコネクションを作成
var connectionString = ConfiguretionManager.ConnectionStrings["DefaltConnection"].ConnectionString;
using (var connection = new SqlConnection(connectionsString))
using (var command = connection.CreateCommand())
{
connection.Open();
// 採番テーブルを利用し採番処理を行う
command.CommandText = "SELECT sql = (NEXT VALUE FOR UserSeq)"
}
using (var render = command.ExeuteTeader())
{
if (render.Read())
{
var rawSeqId= reader["seq"];
seqId = rawSeqId.ToString();
} else { throw new Exception(); }
}
id = new UserId(seqId);
this.name = name;
}
}
インスタンス生成の処理をファクトリに記述したことで、Userクラスをインスタンス化する際には必ず外部からUserIdが引き渡されることになる。