ドメイン駆動設計をシンプルに:開発者の視点
Wenhao Wang
Dev Intern · Leapcell

私たちの日常的な開発作業では、DDDについてよく耳にします。しかし、DDDとは一体何なのでしょうか?
これまでにもオンラインで多くの記事がありましたが、そのほとんどが長くて理解しにくいものでした。この記事では、DDDとは何かについて、より明確なイメージを持っていただくことを目的としています。
DDDとは?
DDD(ドメイン駆動設計)は、ビジネスドメインに焦点を当てることで複雑なシステムを構築するためのソフトウェア開発手法です。その中心となる考え方は、コード構造を実際のビジネスニーズと緊密に統合することです。
一言で言うと、DDDとは、単に機能を実装するのではなく、コードを使ってビジネスの本質を反映することです。
- 従来の開発では、PRDドキュメントに従ってif-elseロジックを記述します(データベースの設計方法によってコードの記述方法が決まります)。
- DDDでは、ビジネス関係者と協力してドメインモデルを構築します。コードはビジネスを反映します(ビジネスが変化すると、コードもそれに応じて適応します)。
従来の開発モデル:簡単な登録の例
正直なところ、抽象的な概念は時間が経つと忘れやすいですよね?コードの例を見てみましょう。
次のようなビジネスルールを持つユーザー登録機能を構築するとします。
- ユーザー名は一意である必要があります
- パスワードは複雑さの要件を満たす必要があります
- 登録後にログを記録する必要があります
従来の開発では、次のようなコードをすぐに書くかもしれません。
@Controller public class UserController { public void register(String username, String password) { // パスワードの検証 // ユーザー名のチェック // データベースに保存 // ログの記録 // すべてのロジックが混ざり合っている } }
「すべてのコードがコントローラーにあるわけがないでしょう。コントローラー、サービス、DAOなどのレイヤーを使用して関心を分離する必要があります」と言う人もいるかもしれません。したがって、コードは次のようになる可能性があります。
// サービス層:フローのみを制御し、ビジネスルールは分散している public class UserService { public void register(User user) { // 検証ルール1:ユーティリティクラスで実装 ValidationUtil.checkPassword(user.getPassword()); // 検証ルール2:アノテーションを介して実装 if (userRepository.exists(user)) { ... } // データはDAOに直接渡される userDao.save(user); } }
公平を期すために言うと、このバージョンはフローがはるかに明確です。興奮して「おい、すでにコードをレイヤー化しているぞ!エレガントでクリーンに見える。これはDDDに違いない!」と言う人もいるかもしれません。
レイヤー化はDDDと同じですか?
答えはノーです!
上記のコードはレイヤー化され、構造的に分割されていますが、DDDではありません。
その従来のレイヤー化されたコードでは、Userオブジェクトは単なるデータキャリア(貧血モデル)であり、ビジネスロジックは他の場所にオフロードされています。DDDでは、一部のロジックはドメインオブジェクト内にカプセル化する必要があります。たとえば、パスワードの検証などです。
この登録の例では、DDDのアプローチ(リッチモデル)は次のようになります。
// ドメインエンティティ:ビジネスロジックをカプセル化 public class User { public User(String username, String password) { // パスワードルールはコンストラクターにカプセル化されている if (!isValidPassword(password)) { throw new InvalidPasswordException(); } this.username = username; this.password = encrypt(password); } // パスワードの複雑さの検証はエンティティの責任 private boolean isValidPassword(String password) { ... } }
ここでは、パスワードの検証がUserドメインエンティティにプッシュダウンされています。専門用語では、ビジネスルールはドメインオブジェクト内にカプセル化されています。オブジェクトは単なる「データの塊」ではなくなりました。
DDDの主要な設計概念
では、DDDは単にいくつかのロジックをドメインオブジェクトにプッシュすることだけなのでしょうか?
正確にはそうではありません。
レイヤー化に加えて、DDDの本質は、次のパターンを通じてビジネス表現を深めることにあります。
- 集約ルート
- ドメインサービスとアプリケーションサービス
- ドメインイベント
集約ルート
シナリオ:ユーザー(User
)は配送先住所(Address
)に関連付けられています
- 従来のアプローチ:
User
とAddress
をサービス層で個別に管理 - DDDのアプローチ:
User
を集約ルートとして扱い、それを通じてAddress
の追加/削除を制御
public class User { private List<Address> addresses; // アドレスを追加するロジックは集約ルートによって制御される public void addAddress(Address address) { if (addresses.size() >= 5) { throw new AddressLimitExceededException(); } addresses.add(address); } }
ドメインサービスとアプリケーションサービス
- ドメインサービス:複数のエンティティにまたがるビジネスロジックを処理します(例:2つのアカウント間でお金を送金する)
- アプリケーションサービス:プロセス全体を調整します(例:ドメインサービスの呼び出し+メッセージの送信)
// ドメインサービス:コアビジネスロジックを処理 public class TransferService { public void transfer(Account from, Account to, Money amount) { from.debit(amount); // デビットロジックはAccountエンティティにカプセル化されている to.credit(amount); } } // アプリケーションサービス:プロセスを調整し、ビジネスロジックは含まない public class BankingAppService { public void executeTransfer(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId); Account to = accountRepository.findById(toId); transferService.transfer(from, to, new Money(amount)); messageQueue.send(new TransferEvent(...)); // インフラストラクチャ操作 } }
ドメインイベント
イベントを使用して、ビジネス状態の変化を明示的に表現します。
例:ユーザーが正常に登録された後、UserRegisteredEvent
をトリガーします
public class User { public void register() { // ...登録ロジック this.events.add(new UserRegisteredEvent(this.id)); // ドメインイベントを記録 } }
従来の開発とDDDの違い
従来の開発とDDDの違いを簡単にまとめましょう。
従来の開発:
- **ビジネスロジックの所有権:**サービス、ユーティリティ、コントローラーに分散
- **モデルの役割:**データキャリア(貧血モデル)
- **技術的な実装への影響:**スキーマはデータベーステーブルの設計によって駆動される
DDD:
- **ビジネスロジックの所有権:**ドメインエンティティまたはドメインサービスにカプセル化
- **モデルの役割:**動作を伴うビジネスモデル(リッチモデル)
- **技術的な実装への影響:**スキーマはビジネスニーズによって駆動される
DDDの例:eコマースの注文
よりよく理解していただくために、具体的なDDDのケースを紹介して「喉の渇きを癒しましょう」。
次のような要件があるとします。
注文を行う際、システムは、在庫の検証、クーポンの適用、実際の支払額の計算、注文の生成を行う必要があります。
従来の実装(貧血モデル)
// サービス層:肥大化した注文の配置ロジック public class OrderService { @Autowired private InventoryDAO inventoryDAO; @Autowired private CouponDAO couponDAO; public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) { // 1. 在庫の検証(サービスに分散) for (ItemDTO item : items) { Integer stock = inventoryDAO.getStock(item.getSkuId()); if (item.getQuantity() > stock) { throw new RuntimeException("在庫が不足しています"); } } // 2. 合計金額の計算 BigDecimal total = items.stream() .map(i -> i.getPrice().multiply(i.getQuantity())) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. クーポンの適用(ロジックはユーティリティクラスに隠されている) if (couponId != null) { Coupon coupon = couponDAO.getById(couponId); total = CouponUtil.applyCoupon(coupon, total); // 割引ロジックはユーティリティにある } // 4. 注文の保存(純粋なデータ操作) Order order = new Order(); order.setUserId(userId); order.setTotalAmount(total); orderDAO.save(order); return order; } }
従来のアプローチの問題点:
- 在庫の検証とクーポンのロジックは、サービス、ユーティリティ、DAOに分散されています
Order
オブジェクトは単なるデータキャリア(貧血)であり、ビジネスルールを所有する人はいません- 要件が変更された場合、開発者はサービス層を「掘り下げる」必要があります
DDDの実装(リッチモデル):ビジネスロジックはドメインにカプセル化
// 集約ルート:Order(コアロジックを担う) public class Order { private List<OrderItem> items; private Coupon coupon; private Money totalAmount; // ビジネスロジックはコンストラクターにカプセル化されている public Order(User user, List<OrderItem> items, Coupon coupon) { // 1. 在庫の検証(ドメインルールはカプセル化されている) items.forEach(item -> item.checkStock()); // 2. 合計金額の計算(ロジックは値オブジェクト内にある) this.totalAmount = items.stream() .map(OrderItem::subtotal) .reduce(Money.ZERO, Money::add); // 3. クーポンの適用(ルールはエンティティにカプセル化されている) if (coupon != null) { validateCoupon(coupon, user); // クーポンルールはカプセル化されている this.totalAmount = coupon.applyDiscount(this.totalAmount); } } // クーポンの検証ロジック(ドメインによって明確に所有されている) private void validateCoupon(Coupon coupon, User user) { if (!coupon.isValid() || !coupon.isApplicable(user)) { throw new InvalidCouponException(); } } } // ドメインサービス:注文プロセスを調整 public class OrderService { public Order createOrder(User user, List<Item> items, Coupon coupon) { Order order = new Order(user, convertItems(items), coupon); orderRepository.save(order); domainEventPublisher.publish(new OrderCreatedEvent(order)); // ドメインイベント return order; } }
DDDアプローチの利点:
- 在庫の検証:
OrderItem
値オブジェクトにカプセル化 - クーポンのロジック:
Order
エンティティのメソッド内にカプセル化 - 計算ロジック:
Money
値オブジェクトによる精度の確保 - **ビジネスの変更:**ドメインオブジェクトの変更のみが必要
ここで、新しい製品要件があるとします。クーポンは「100ドル以上の注文に20ドル割引」を提供し、新規ユーザーにのみ適用されます。
従来の開発では、以下を変更する必要があります。
CouponUtil.applyCoupon()
ロジック- 新規ユーザーの検証を追加するためのサービス層
DDDでは、以下のみを変更する必要があります。
- ドメイン層の
Order.validateCoupon()
メソッド
DDDはいつ使用すべきか?
では、DDDはあらゆる状況で使用すべきでしょうか?そうではありません。それは過剰なエンジニアリングになります。
- ✅ ビジネスが複雑な場合(例:eコマース、金融、ERP)
- ✅ 要件が頻繁に変更される場合(インターネットビジネスの90%)
- ❌ 単純なCRUDの場合(管理パネル、データレポート)
この引用は非常に理にかなっていると思います。
ビジネスルールを変更するために、コントローラーやDAOに触れることなく、ドメイン層の変更のみが必要な場合、それがDDDが真に実装されたときです。
Webバックエンドプロジェクトのホスティングには、Leapcellをご利用ください。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ料金を支払います。リクエストも料金もかかりません。
比類のない費用対効果
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用オーバーヘッドはゼロです。構築に集中してください。
詳細については、ドキュメントをご覧ください。
Xでフォローしてください:@LeapcellHQ