重い腰を上げてドメイン駆動設計入門してみた
はじめに
私は現在、小規模な受託系の企業でせっせと開発をしてるよわよわエンジニアのせつです。
自社の悪口を言うわけではありませんがコードは質よりスピードが評価されるような環境なため、「このままではヤバくないか?」と思い、SNS等でも評価が高かったドメイン駆動設計入門を読んだので私なりに重要な部分をまとめて感想を書いていきたいと思いました。
これ以外にもDDDとえいば「エリック・エヴァンスのドメイン駆動設計」が定番中の定番ですが、本屋で中身を見たら挫折しそうだったので飛び級せずにレベルに合ったものを手に取りました。
それにしても正月休みは読書が捗る...
結論
最初にこの本を読んで感じたことをまとめると、ドメイン駆動設計は自分が想像していたより特別なものでも魔法のような設計手法でもないなと感じました。これは悪口でもなんでもなくコーディングをするときに「確かに大事だよね」と思うようなものが凝縮されているからだと思いました。でもそれを実践するのは本当に難しく、知ってるのとできるのとでは圧倒的な壁があるなとも感じました。ドメイン駆動設計を学ぶことで「なぜこの設計にしたのか」「どのようにこのシステムはビジネスを支えるのか」を意識する姿勢が身につくという点でも、エンジニアとしての成長に直結する学びだと感じました。
ドメインってナニモノ?
この界隈はやたらドメインドメインと言われるが一体ナニモノなんだろうか?
本書ではソフトウェア開発におけるドメインとは「領域」、つまりプログラムを適用する領域のことをドメインと言うらしい。
例えば、「物流システム」を開発する場合、ドメインは「物流」という領域になります。この中には、「在庫」「配送」「注文」など、さらに細分化されたサブドメインが含まれています。このエンティティ達をうまく取りまとめてコードに落とし込むことで保守性が高いシステムになる。
ソフトウェア開発において「ドメイン」を理解することは、業務知識を深め、開発するシステムが実際の業務でどう使われるかを意識することに繋がります。これにより、現実の問題を効果的に解決できる設計が可能になります。
こんな具合で、開発者はドメインを深く理解することで利用者にとって意味のあるソフトウェアを作れるようになると言うのがドメイン駆動設計のようです。
ドメインモデルとは
まずは本書から引用します。
モデルとは現実の事象やあるいは概念を抽象化した概念
これだけでは具体的にどういうものかイメージが湧かない方も多いかもしれません。そこで、少し噛み砕いて説明します。
例えば、物流システムの「ドメイン(問題領域)」を考えてみましょう。この場合、「在庫」や「注文」といった現実世界の概念が、システム上でも重要な要素になります。しかし、単にそれらの名前を並べただけではシステムの設計にはなりません。
ドメインモデルとは、これらの概念をさらに整理し、具体的な「ふるまい(操作やルール)」も含めて、システムで扱える形に抽象化したものです。
例えば、物流システムで「在庫」というドメインを考えると、以下のような要素をモデリングします。
概念(属性):
「在庫ID」「在庫数」「倉庫の場所」などの属性。
ふるまい(操作):
「在庫を追加する」「在庫を引き当てる」「在庫数を確認する」などの操作やルール。
こうした属性や操作を整理し、システム上で扱える「設計図」にするのがドメインモデルの役割です。
ドメインオブジェクトとは
メインオブジェクトとは、先ほど説明した ドメインモデルをコードで具体化した要素 を指します。ドメインモデルが「設計図」であるならば、ドメインオブジェクトは「設計図に基づいて作られた実際の建物」みたいなイメージでしょうか。
以下はドメインオブジェクトの一例です。
class Item {
private ItemId $id;
private ItemName $name;
public function __construct(ItemId $id, ItemName $name) {
$this->id = $id;
$this->name = $name;
}
public function getId(): ItemId {
return $this->id;
}
public function getName(): ItemName {
return $this->name;
}
}
このように、ドメインの概念 → ドメインモデル → ドメインオブジェクト の順で設計を行うことで、現実世界のドメインをコードに落とし込んでいく手法こそ、ドメイン駆動設計(DDD)です。
ドメインの概念が変化すれば、ドメインモデルも変化し、それがドメインオブジェクトに反映されます。この設計の流れにより、システムはビジネスの本質を反映し続け、変化にも柔軟に対応できるようになります。
例えば、物流システムで「在庫数」という概念が「個数」から「重量」に変更された場合(多分あり得ない)、ドメインモデルでは「在庫数」を「在庫単位」に変更し、それが「在庫オブジェクト」の属性にも反映されます。これによってビジネスとシステムのズレを最小限に抑えられます。
値オブジェクトとエンティティ
恐らくドメイン駆動設計の核となるのが値オブジェクトとエンティティではないでしょうか。
私の認識では、エンティティとは先ほどの物流システムでいうところの在庫や注文などを指します。そしてそのエンティティが持つ属性を安全に表現するために値オブジェクトが存在します。
以下はPHPで作った値オブジェクトとエンティティのサンプルです。
<?php
// 値オブジェクト: ItemId
class ItemId {
private int $value;
public function __construct(int $itemId) {
if ($itemId <= 0) {
throw new InvalidArgumentException("ItemId must be positive.");
}
$this->value = $itemId;
}
public function getValue(): int {
return $this->value;
}
}
// 値オブジェクト: ItemName
class ItemName {
private string $value;
public function __construct(string $itemName) {
if (empty(trim($itemName))) {
throw new InvalidArgumentException("ItemName cannot be empty.");
}
$this->value = $itemName;
}
public function getValue(): string {
return $this->value;
}
}
// エンティティ: Item
class Item {
private ItemId $id;
private ItemName $name;
public function __construct(ItemId $id, ItemName $name) {
$this->id = $id;
$this->name = $name;
}
public function getName(): string {
return $this->name->getValue();
}
}
// 利用例
$item = new Item(new ItemId(1), new ItemName("Sample Item"));
echo "Item Name: " . $item->getName() . PHP_EOL;
このようにあえてプリミティブな値を使わずに、オブジェクトとして適用することでItemが生成された時点で不正なIDや名前が存在しないことを担保できます。スピード命の現場で働いてる自分からするとこれは驚愕でした。ドメインを表現するために大量のファイルを作成するんですから...
これのメリットはズバリロジックが分散しないことですね。
サンプルにはないですが、このシステムでは商品名の文字数が30文字以下である必要がある場合どうしたらいいか?そのロジックをItemNameに含めてしまえば良いのです。これによって他の部分でも文字数チェックロジックを書く必要がなくなります。仮に仕様変更で40文字以下に変更されてもItemNameの文字数チェックのみ変更すれば全てのプログラムに適用されるようになります。大きなプロジェクトになればなるほどその威力は絶大な訳です。逆にこのロジックがありとあらゆる場所に分散していたらと思うとゾッとします。
ドメインサービス
ドメインサービスとは値オブジェクトとエンティティに記述するには不自然なふるまいなどをドメインサービスに記述します。
言葉ではわかりにくいのでまずは、ドメインサービスに書くべきロジックをエンティティに記述した例です。
// エンティティ: Item
class Item {
private ItemId $id;
private ItemName $name;
public function __construct(ItemId $id, ItemName $name) {
$this->id = $id;
$this->name = $name;
}
public function getId(): int {
return $this->id->getValue();
}
public function getName(): string {
return $this->name->getValue();
}
// 同じ商品かどうかを判定するロジック
public function isSameAs(Item $otherItem): bool {
return $this->getId() === $otherItem->getId();
}
}
isSameAsはシンプルな重複チェックです。
// 利用例
$item1 = new Item(new ItemId(1), new ItemName("Sample Item 1"));
$item2 = new Item(new ItemId(2), new ItemName("Sample Item 2"));
$item3 = new Item(new ItemId(1), new ItemName("Another Sample Item"));
echo $item1->isSameAs($item2) ? "Same Item" : "Different Items"; // Output: Different Items
echo PHP_EOL;
echo $item1->isSameAs($item3) ? "Same Item" : "Different Items"; // Output: Same Item
着目して欲しいの呼び出し時のコードです。
$item1->isSameAs($item2)
重複をチェックするために自らのオブジェクトに問い合わせています。現実世界で言うと「俺って俺ですか?」「私は私ですよね?」と自分に問い合わせてる状態になります。だいぶおかしな人になってしまいます。そういったロジックはドメインサービスに集約させましょう。
// ドメインサービス: ItemService
class ItemService {
public function isSameItem(Item $item1, Item $item2): bool {
return $item1->getId() === $item2->getId();
}
}
// 呼び出し例
$item1 = new Item(new ItemId(1), new ItemName("Item A"));
$item2 = new Item(new ItemId(2), new ItemName("Item B"));
$item3 = new Item(new ItemId(1), new ItemName("Item A (Duplicate)"));
$itemService = new ItemService();
echo "Comparing Item 1 and Item 2: ";
echo $itemService->isSameItem($item1, $item2) ? "Same Item" : "Different Items"; // Output: Different Items
このようにItemServiceに責務を委譲することで自分自身に問い合わせするような不自然さが解消されます。こんな感じでドメインオブジェクトに実装すると不自然な処理を解消できますね。
アプリケーションサービス
では次にユースケースを実現するアプリケーションサービスを実装しましょう。
アプリケーションサービスとはいわば指揮官です。これまで作成してきたドメインオブジェクト達に指示を出し、ユースケースを実現することを目的とします。
では「今回は商品を作成する」と言うユースケースを表現しましょう。
早速コード例を見てみましょう。
// アプリケーションサービス
class ItemApplicationService {
private ItemRepository $repository;
private ItemService $checker;
public function __construct(ItemRepository $repository, ItemService $checker) {
$this->repository = $repository;
$this->checker = $checker;
}
public function createItem(int $id, string $name): void {
$itemName = new ItemName($name);
if ($this->checker->isDuplicate($itemName)) {
throw new RuntimeException("Item with the same name already exists.");
}
$item = new Item(new ItemId($id), $itemName);
$this->repository->save($item);
}
}
コードの説明をするとまず、コンストラクタで先ほど作成したItemService(ドメインサービス)とデータベースへのアクセスを行うItemRepositoryをセットします。
そして、createItemメソッド内で$this->checker->isDuplicate($itemName)
と言う記述で重複チェック(実際の処理はここでは解説しません)を行い、最後にリポジトリで永続化をしています。
こんな感じでこれまで作成してきたオブジェクトを総動員して、ユースケースを達成するのがアプリケーションサービスです。この層はあくまで処理の呼び出しを行っているだけでロジックの詳細はドメインオブジェクトが責任を負います。こんな感じでコードに落とし込んでいき、疎結合、高凝集な設計を実現することが重要になります。
感想
ドメインという抽象的な概念がこの設計手法を難解にさせてる気がしました。
実際に行っていることは適切にクラスを分割して、適切なクラスにロジックを集約させるというシンプルな設計手法だなと。ただ実際のソフトウェア開発は今回紹介した例よりも遥かに複雑なため正しく設計する難易度は非常に高いと感じました。今回紹介しなかったリポジトリやファクトリといったパターンもあるので気になる方はぜひ本書を手に取ってみてください。私もDDDに対する苦手意識が薄れてきたので、時間がある時にエヴァンス本にも挑戦したいと思います。
Discussion