T-CREATOR

Devin と進めるドメイン駆動設計:ユビキタス言語を反映させるプロンプト構成

Devin と進めるドメイン駆動設計:ユビキタス言語を反映させるプロンプト構成

AI エージェント「Devin」を活用したドメイン駆動設計(DDD)の実践において、最も重要なのは「ユビキタス言語」をプロンプトに正確に反映させることです。この記事では、Devin とのコミュニケーションを通じて、ドメインの専門用語やビジネスロジックを一貫性のある形でコード化する具体的な方法をご紹介します。

プロジェクトの成功は、開発チームとビジネスチームが同じ言語で会話できるかにかかっています。Devin という AI パートナーを活用することで、この「共通言語」をコードに落とし込む作業が驚くほどスムーズになるでしょう。

背景

ドメイン駆動設計とユビキタス言語の関係性

ドメイン駆動設計(Domain-Driven Design, DDD)は、エリック・エヴァンスによって提唱された設計手法で、ビジネスドメインの複雑さをソフトウェアで表現することを目的としています。その中核となる概念が「ユビキタス言語(Ubiquitous Language)」です。

ユビキタス言語とは、開発者とドメインエキスパート(ビジネス側の専門家)が共通して使用する言葉のことを指します。この言語は、会議での会話、ドキュメント、そしてコードにまで一貫して使われることで、認識のずれを防ぎます。

従来の開発では、ビジネス用語と技術用語の間に翻訳のレイヤーが存在し、それが誤解や実装ミスの原因となってきました。DDD では、この翻訳を排除し、ドメインの言葉をそのままコードに反映させることを重視します。

以下の図は、ユビキタス言語がどのように開発プロセス全体に浸透するかを示しています。

mermaidflowchart TB
  domain["ドメインエキスパート<br/>(ビジネス側)"]
  dev["開発チーム"]
  ub["ユビキタス言語<br/>共通の用語集"]

  domain -->|"ビジネス要件を<br/>共通言語で表現"| ub
  dev -->|"コードに<br/>共通言語を反映"| ub
  ub -->|"一貫性のある<br/>コミュニケーション"| domain
  ub -->|"理解しやすい<br/>実装"| dev

この図が示すように、ユビキタス言語は開発者とドメインエキスパートをつなぐ架け橋となります。言葉の統一により、仕様の誤解が減り、コードの可読性も向上するのです。

Devin を DDD に活用する意義

Devin は、Cognition AI 社が開発した AI エンジニアリングアシスタントで、コード生成からデバッグ、テスト作成まで幅広いタスクを自律的に実行できます。このツールを DDD と組み合わせることで、以下のメリットが得られます。

まず、ドメインモデルの迅速な実装が可能になります。ユビキタス言語を含むプロンプトを与えることで、Devin はビジネスロジックを反映したエンティティや値オブジェクトを自動生成してくれるでしょう。

次に、言語の一貫性を保つサポートが期待できます。プロンプトに用語集や命名規則を含めることで、Devin は既存のコードベースと統一された表現を使用してくれます。

さらに、リファクタリングの効率化も見逃せません。ドメインの理解が深まるにつれて用語が変更されることがありますが、Devin を使えば大規模な用語変更も短時間で実施できるのです。

下表は、従来の手動実装と Devin を活用した実装の違いを比較したものです。

#項目手動実装Devin 活用
1ドメインモデル作成時間数時間〜数日数分〜数時間
2用語の一貫性確保レビュー頼みプロンプト設計で自動化
3リファクタリング工数高い(手作業)低い(一括変更可能)
4ドメイン知識の反映開発者の理解に依存プロンプトで明示的に指定
5テストコード生成手動で作成同時生成可能

この比較からわかるように、Devin を活用することで、DDD の実践における多くの課題を解決できます。

課題

プロンプトにおけるユビキタス言語の欠如

Devin のような AI ツールを DDD に活用する際、最大の課題となるのが「プロンプトにユビキタス言語が反映されていない」という問題です。

多くの開発者は、Devin に対して技術的な指示のみを与えてしまいがちです。例えば「ユーザー管理機能を作って」というプロンプトでは、ビジネスドメイン固有の文脈が全く伝わりません。

この結果、以下のような問題が発生します。まず、生成されたコードが汎用的すぎて、ドメインの特性を反映していないケースです。「User」というクラス名は確かに作られますが、それが「会員」なのか「顧客」なのか「利用者」なのか、ビジネス上の意味が失われてしまいます。

次に、技術用語とビジネス用語の混在が起こります。Devin が生成したコードに「Member」と書かれているのに、ドキュメントでは「顧客」と表現されているといった不一致が生じるのです。

さらに深刻なのは、ドメインロジックの誤実装です。ビジネスルールが正しく伝わっていないため、Devin が生成したコードが仕様を満たしていないことがあります。

以下の図は、不適切なプロンプトがどのように問題を引き起こすかを示しています。

mermaidflowchart LR
  dev["開発者"] -->|"技術的な指示のみ"| prompt["不完全なプロンプト"]
  prompt -->|"ドメイン文脈なし"| devin["Devin"]
  devin -->|"汎用的すぎる実装"| code["生成コード"]
  code -->|"ビジネスロジック<br/>との乖離"| gap["仕様との不一致"]
  gap -->|"手動修正が必要"| rework["手戻り作業"]

この図が示すように、プロンプトの質が低いと、最終的には手戻り作業が増えてしまうのです。

ドメインエキスパートとの認識ギャップ

もう一つの大きな課題は、ドメインエキスパート(ビジネス側の専門家)と開発者の間に存在する認識のギャップです。

ドメインエキスパートは、自分たちの業務用語でシステムを説明します。例えば、EC サイトの場合「カートに商品を入れる」「注文を確定する」「配送手配をする」といった表現を使うでしょう。

一方、開発者は技術的な視点でシステムを捉えます。「カート機能」を「セッション管理」や「一時保存機能」と捉え、「注文確定」を「トランザクション処理」として実装してしまうかもしれません。

この認識ギャップが Devin へのプロンプトにも反映されると、以下のような問題が起こります。

まず、生成されたコードがビジネス側にとって理解不能になります。技術用語で書かれたコードは、ドメインエキスパートがレビューすることができず、仕様の妥当性を確認できないのです。

次に、要件の誤解によるバグが多発します。開発者が自分の解釈でプロンプトを作成すると、ビジネスルールが正しく反映されず、リリース後に重大な問題が発覚することがあります。

さらに、保守性の低下も無視できません。コードとドメインの言葉が一致していないと、将来の機能追加や変更時に、開発者がコードの意図を理解するのに時間がかかってしまいます。

下表は、認識ギャップがもたらす具体的な問題をまとめたものです。

#ビジネス用語誤った技術的解釈発生する問題
1会員登録ユーザー作成処理会員特典ロジックの欠落
2注文確定データ保存処理在庫確保や決済処理の漏れ
3配送手配ステータス更新配送業者連携の実装不足
4キャンセル受付データ削除処理返金処理や在庫戻しの欠如
5ポイント付与数値加算処理付与条件やルールの未実装

このように、用語の不一致は単なる命名の問題ではなく、ビジネスロジックの欠落につながる深刻な課題なのです。

AI が生成するコードの品質管理

Devin が生成したコードは、一見すると動作するように見えても、ドメインの観点から見ると不適切な実装になっていることがあります。

AI は学習データに基づいてコードを生成しますが、プロジェクト固有のドメインルールまでは理解していません。そのため、プロンプトに明示されていないビジネス制約を無視した実装が生まれる可能性があります。

例えば、「商品を注文する機能」を依頼した場合、Devin は基本的な注文処理を生成するでしょう。しかし、「在庫がない場合は予約注文として扱う」「会員ランクによって割引率が変わる」といったドメイン固有のルールは、プロンプトに明記しなければ実装されません。

また、ドメインオブジェクトの責務が曖昧になる問題もあります。Devin が生成したエンティティに、本来サービス層が持つべきロジックが含まれていたり、逆に必要なビジネスルールが欠けていたりすることがあるのです。

品質管理の課題は以下の点に集約されます。

ビジネス制約の漏れ:プロンプトで指定しない限り、ドメイン固有のルールが実装されない

不変条件(インバリアント)の欠如:エンティティが守るべき状態の一貫性が保証されない

値オブジェクトの不適切な実装:プリミティブ型をそのまま使い、ドメインの意味が失われる

これらの問題を防ぐには、プロンプト設計の段階でユビキタス言語とビジネスルールを明確に伝える必要があります。

解決策

ユビキタス言語を中核としたプロンプト設計

Devin を活用した DDD の実践では、プロンプトにユビキタス言語を体系的に組み込むことが解決の鍵となります。

効果的なプロンプト設計には、以下の要素を含める必要があります。まず、ドメイン用語の定義を明確にすることです。ビジネスで使われる専門用語を、その意味とともにプロンプトに記載しましょう。

次に、用語間の関係性を示すことが重要です。「注文」と「注文明細」、「会員」と「会員ランク」といった概念の関連を説明することで、Devin はより正確なモデルを生成できます。

さらに、ビジネスルールを明示的に記述します。「在庫数を超える注文は受け付けない」「会員ランクがゴールド以上の場合は送料無料」といった制約を、プロンプトに含めるのです。

以下は、ユビキタス言語を反映させた具体的なプロンプトの構成例です。

typescript// プロンプト設計の基本構造(TypeScript で表現)

interface DomainPromptStructure {
  // ドメイン用語の定義
  ubiquitousLanguage: {
    term: string; // 用語名
    definition: string; // 定義
    businessContext: string; // ビジネス上の文脈
  }[];

  // エンティティとその責務
  entities: {
    name: string; // エンティティ名(ドメイン用語を使用)
    identity: string; // 識別子
    invariants: string[]; // 不変条件
    behaviors: string[]; // ビジネス上の振る舞い
  }[];

  // 値オブジェクト
  valueObjects: {
    name: string; // 値オブジェクト名
    validation: string[]; // バリデーションルール
    meaning: string; // ドメイン上の意味
  }[];
}

このように、プロンプト自体をドメインモデルの仕様書として構造化することで、Devin は一貫性のあるコードを生成できるようになります。

プロンプトの前提情報セクション

Devin に指示を出す前に、ドメインの前提情報を整理しましょう。

markdown# ドメイン:EC サイトの注文管理

# ユビキタス言語

- **会員**:サイトに登録済みのユーザー。会員番号で識別される
- **注文**:会員が商品を購入する行為。注文番号で一意に識別される
- **注文明細**:注文に含まれる個々の商品情報。商品、数量、単価を持つ
- **在庫**:倉庫にある商品の数量。注文時に確保される
- **配送手配**:注文確定後、配送業者に商品の発送を依頼すること

# ビジネスルール

1. 注文は必ず会員によって行われる
2. 注文確定時に在庫を確保する。在庫不足の場合はエラー
3. 会員ランクがゴールド以上の場合、送料を無料とする
4. 注文金額が 5,000 円以上の場合、ポイントを 5% 付与する

このような前提情報を最初に提示することで、Devin はドメインの文脈を理解した上でコード生成を開始できます。

具体的な実装指示の書き方

前提情報を共有した後、具体的な実装タスクを指示します。

markdown# タスク:注文エンティティの実装

以下のユビキタス言語に基づいて、注文(Order)エンティティを TypeScript で実装してください。

# 注文エンティティの仕様

- **識別子**:OrderId(値オブジェクトとして実装)
- **属性**  - 会員(Member エンティティへの参照)
  - 注文明細リスト(OrderItem の配列)
  - 注文日時(OrderedAt 値オブジェクト)
  - 注文ステータス(OrderStatus 値オブジェクト)

- **不変条件**  - 注文明細は最低 1 つ必要
  - 注文金額は各明細の小計の合計
  - ステータスは「受付中」→「確定」→「配送中」→「完了」の順でのみ遷移可能

- **ビジネスロジック**  - `confirm()`: 注文を確定し、在庫を確保する
  - `calculateTotal()`: 注文金額を計算する(会員ランクによる割引を考慮)
  - `addItem()`: 注文明細を追加する(在庫チェック付き)

このように、ドメインの言葉でエンティティの仕様を記述することで、Devin は DDD の原則に沿ったコードを生成できるのです。

以下の図は、ユビキタス言語を反映したプロンプトから、ドメインモデルが生成されるまでの流れを示しています。

mermaidflowchart TB
  lang["ユビキタス言語<br/>用語定義"]
  rules["ビジネスルール<br/>制約条件"]
  prompt["構造化プロンプト"]
  devin["Devin"]
  entity["エンティティ<br/>実装"]
  vo["値オブジェクト<br/>実装"]
  service["ドメインサービス<br/>実装"]

  lang --> prompt
  rules --> prompt
  prompt --> devin
  devin --> entity
  devin --> vo
  devin --> service

  entity -.->|"ドメイン用語を<br/>クラス名・メソッド名に反映"| lang
  vo -.->|"ビジネス上の意味を<br/>型で表現"| lang
  service -.->|"ビジネスルールを<br/>ロジックとして実装"| rules

この図が示すように、ユビキタス言語とビジネスルールをプロンプトに組み込むことで、Devin が生成するコードは自然とドメインの意味を反映したものになります。

ドメインモデルパターンの活用

DDD には、エンティティ、値オブジェクト、集約、ドメインサービスといった戦術的パターンが存在します。これらのパターンをプロンプトに明示することで、Devin はより洗練されたドメインモデルを生成できるでしょう。

エンティティのプロンプト設計

エンティティは、識別子によって区別されるドメインオブジェクトです。

markdown# エンティティ実装:会員(Member)

会員エンティティを実装してください。

# 識別子

- MemberId(UUID を内部に持つ値オブジェクト)

# 属性

- 会員番号(MemberNumber 値オブジェクト)
- 氏名(MemberName 値オブジェクト)
- メールアドレス(Email 値オブジェクト)
- 会員ランク(MemberRank 列挙型)
- 登録日(RegisteredAt 値オブジェクト)

# 振る舞い

- `upgradeRank()`: 会員ランクを昇格させる(ビジネスルールに基づく)
- `updateEmail()`: メールアドレスを変更する(形式バリデーション付き)
- `calculateDiscount()`: 会員ランクに応じた割引率を返す

Devin にこのプロンプトを与えることで、単なるデータクラスではなく、ビジネスロジックを持ったエンティティが生成されます。

値オブジェクトのプロンプト設計

値オブジェクトは、属性の集合によって識別されるドメインオブジェクトです。

markdown# 値オブジェクト実装:注文金額(OrderAmount)

注文金額を表す値オブジェクトを実装してください。

# 要件

- 内部値は number 型(円単位)
- 不変(Immutable)である
- 負の値は許容しない

# バリデーション

- 金額は 0 以上
- 小数点以下は持たない(整数のみ)

# ビジネスロジック

- `add(amount: OrderAmount)`: 金額を加算した新しいインスタンスを返す
- `applyDiscount(rate: number)`: 割引率を適用した新しいインスタンスを返す
- `isAboveThreshold(threshold: number)`: 閾値を超えているか判定

# 実装パターン

- コンストラクタはプライベートにし、静的ファクトリメソッド `create()` で生成
- 等価性は内部値で判定する `equals()` メソッドを実装

このように、値オブジェクトの特性(不変性、等価性など)をプロンプトに含めることで、DDD の原則に忠実な実装が得られます。

集約のプロンプト設計

集約は、一貫性境界を持つエンティティと値オブジェクトの集まりです。

markdown# 集約実装:注文集約(Order Aggregate)

注文を集約ルートとした集約を実装してください。

# 集約ルート

- Order エンティティ

# 集約内のオブジェクト

- OrderItem(注文明細)エンティティのリスト
- OrderAmount(注文金額)値オブジェクト
- OrderStatus(注文ステータス)値オブジェクト

# 不変条件(集約全体で保証)

- 注文明細は最低 1 つ必要
- 全ての注文明細の在庫が確保されている
- 注文金額は明細の合計と一致する

# 外部アクセス制御

- 注文明細への直接アクセスは禁止
- Order エンティティのメソッド経由でのみ明細を操作可能
- `addItem()`, `removeItem()`, `updateQuantity()` を提供

# ライフサイクル管理

- 注文の作成は OrderFactory を通じて行う
- 永続化は OrderRepository を通じて行う

集約の境界と不変条件を明示することで、Devin は一貫性を保証するコードを生成できます。

下表は、各ドメインモデルパターンをプロンプトに反映させる際のポイントをまとめたものです。

#パターンプロンプトに含めるべき要素Devin の生成物
1エンティティ識別子、不変条件、ビジネスロジックドメイン用語を使ったクラス
2値オブジェクト不変性、バリデーション、等価性判定イミュータブルな型
3集約境界、不変条件、外部アクセス制御一貫性を保証する構造
4ドメインサービス複数エンティティにまたがるロジックステートレスなサービスクラス
5リポジトリ永続化の抽象化、集約単位の操作インターフェースと実装

このように、DDD のパターンをプロンプトに組み込むことで、Devin は設計原則に沿った高品質なコードを生成できるのです。

レビューとフィードバックのサイクル

Devin が生成したコードは、必ずドメインエキスパートと共にレビューし、ユビキタス言語との一致を確認する必要があります。

レビューのプロセスは以下のステップで行います。

ステップ 1:用語の一致確認

生成されたコードのクラス名、メソッド名、変数名が、ユビキタス言語と一致しているかをチェックします。

typescript// ❌ ドメイン用語と不一致
class UserOrder {
  private items: CartItem[];

  public submit(): void {
    // 処理
  }
}

// ✅ ユビキタス言語を反映
class Order {
  private orderItems: OrderItem[];

  public confirm(): void {
    // 注文確定処理
  }
}

技術的な用語(submit, items)ではなく、ビジネス用語(confirm, orderItems)が使われているかを確認しましょう。

ステップ 2:ビジネスルールの実装確認

プロンプトで指定したビジネスルールが、正しく実装されているかを検証します。

typescript// ビジネスルール:在庫を超える注文は受け付けない

class OrderItem {
  constructor(
    private product: Product,
    private quantity: number
  ) {
    // ✅ ビジネスルールが実装されている
    if (quantity > product.stockQuantity) {
      throw new Error('在庫不足のため注文できません');
    }
  }
}

このように、ドメインの制約がコードに反映されているかを確認するのです。

ステップ 3:ドメインエキスパートとの対話

生成されたコードをドメインエキスパートに見せ、ビジネスロジックが正しいかをヒアリングします。この際、技術用語は使わず、ユビキタス言語で説明しましょう。

markdown# ドメインエキスパートへの説明例

「この Order クラスは『注文』を表現しています。
confirm() メソッドを呼ぶと『注文確定』が行われ、
在庫が確保されます。会員ランクによって割引が適用され、
最終的な注文金額が計算されます。

この実装は、業務フローと一致していますか?」

専門用語を避け、ビジネスの言葉で説明することで、ドメインエキスパートは実装の妥当性を判断できます。

ステップ 4:フィードバックの反映

レビューで見つかった問題点を、プロンプトに反映させます。

markdown# フィードバックに基づくプロンプト修正

# 修正前のプロンプト

「注文確定時に在庫を確保してください」

# ドメインエキスパートからのフィードバック

「在庫確保だけでなく、仮押さえ期限も設定する必要があります。
確定から 30 分以内に決済されない場合、在庫を解放します」

# 修正後のプロンプト

「注文確定時に在庫を確保してください。
確保した在庫には 30 分の仮押さえ期限を設定し、
期限内に決済されない場合は自動的に在庫を解放する処理を実装してください」

このように、フィードバックをプロンプトに組み込むことで、次回の生成精度が向上します。

以下の図は、レビューとフィードバックのサイクルを示しています。

mermaidflowchart LR
  prompt["プロンプト作成"]
  generate["Devin が<br/>コード生成"]
  review["レビュー<br/>(用語・ルール確認)"]
  expert["ドメインエキスパート<br/>との対話"]
  feedback["フィードバック<br/>収集"]
  refine["プロンプト<br/>改善"]

  prompt --> generate
  generate --> review
  review --> expert
  expert --> feedback
  feedback -->|"問題あり"| refine
  refine --> prompt
  feedback -->|"問題なし"| done["完成"]

このサイクルを繰り返すことで、ドメインモデルの精度が高まり、ユビキタス言語が浸透していきます。

具体例

EC サイトの注文ドメインでの実践

ここでは、EC サイトの注文管理システムを例に、Devin へのプロンプト設計から実装までを具体的に見ていきましょう。

ドメイン分析とユビキタス言語の整理

まず、ドメインエキスパート(EC サイトの運営担当者)と対話し、ユビキタス言語を整理します。

markdown# EC サイト注文ドメインのユビキタス言語

# コアコンセプト

| 用語         | 定義                           | ビジネス上の重要性       |
| ------------ | ------------------------------ | ------------------------ |
| 会員         | サイトに登録したユーザー       | 購入履歴やポイントを管理 |
| ゴールド会員 | 年間 10 万円以上購入した会員   | 送料無料などの特典あり   |
| 注文         | 会員が商品を購入する行為       | 売上の基本単位           |
| 注文明細     | 注文に含まれる個々の商品       | 在庫管理の単位           |
| 在庫確保     | 注文確定時に在庫を予約すること | 二重販売を防ぐ重要処理   |
| 配送手配     | 倉庫に商品の発送を指示すること | 物流との連携ポイント     |
| ポイント付与 | 購入金額に応じてポイントを加算 | 顧客ロイヤリティ向上施策 |

この用語集が、Devin へのプロンプトの基礎となります。

Devin へのプロンプト作成

ユビキタス言語を基に、Devin へのプロンプトを構成します。

markdown# タスク:注文確定処理の実装

# ドメインコンテキスト

以下のユビキタス言語を使用してください:

- **会員(Member)**:サイト登録済みユーザー。会員番号で識別
- **注文(Order)**:会員による商品購入行為。注文番号で識別
- **注文明細(OrderItem)**:注文内の個別商品情報
- **在庫確保(StockReservation)**:注文確定時の在庫予約
- **注文確定(confirm)**:注文を確定し、在庫確保と決済を実行する行為

# 実装要件

## Order エンティティ

以下の仕様で Order エンティティを TypeScript で実装してください。

**識別子**

- OrderId 値オブジェクト(UUID ベース)

**属性**

- memberId: MemberId(会員への参照)
- orderItems: OrderItem[](注文明細リスト)
- orderedAt: Date(注文日時)
- status: OrderStatus(注文ステータス)

**ビジネスロジック**

`confirm()` メソッド:

1. ステータスが「受付中」であることを確認
2. 全ての注文明細の在庫を確保(StockReservation を作成)
3. 在庫不足の場合はエラーをスロー
4. ステータスを「確定済み」に変更
5. ドメインイベント「OrderConfirmed」を発行

`calculateTotal()` メソッド:

1. 全ての注文明細の小計を合算
2. 会員ランクがゴールドの場合、送料を 0 円にする
3. 合計金額を OrderAmount 値オブジェクトで返す

**不変条件**

- 注文明細は最低 1 つ必要
- 確定済みの注文は変更不可

このプロンプトには、ユビキタス言語、ビジネスロジック、不変条件が明確に記載されています。

Devin が生成するコード例

上記のプロンプトに基づき、Devin は以下のようなコードを生成します。

typescript// 値オブジェクト:OrderId

class OrderId {
  private constructor(private readonly value: string) {
    if (!value) {
      throw new Error('注文番号が空です');
    }
  }

  static create(): OrderId {
    return new OrderId(crypto.randomUUID());
  }

  static from(value: string): OrderId {
    return new OrderId(value);
  }

  getValue(): string {
    return this.value;
  }
}

識別子を値オブジェクトとして実装し、不正な値を防いでいます。

typescript// 列挙型:OrderStatus

enum OrderStatus {
  PENDING = '受付中',
  CONFIRMED = '確定済み',
  SHIPPED = '配送中',
  DELIVERED = '配達完了',
  CANCELLED = 'キャンセル済み',
}

ステータスをビジネス用語で定義しています。

typescript// エンティティ:Order

class Order {
  private constructor(
    private readonly orderId: OrderId,
    private readonly memberId: MemberId,
    private orderItems: OrderItem[],
    private status: OrderStatus,
    private readonly orderedAt: Date
  ) {
    // 不変条件:注文明細は最低 1 つ必要
    if (orderItems.length === 0) {
      throw new Error('注文明細が存在しません');
    }
  }

  // ファクトリメソッド
  static create(
    memberId: MemberId,
    orderItems: OrderItem[]
  ): Order {
    return new Order(
      OrderId.create(),
      memberId,
      orderItems,
      OrderStatus.PENDING,
      new Date()
    );
  }

  // ビジネスロジック:注文確定
  confirm(stockService: StockReservationService): void {
    // ステータス確認
    if (this.status !== OrderStatus.PENDING) {
      throw new Error('受付中の注文のみ確定できます');
    }

    // 在庫確保
    for (const item of this.orderItems) {
      const reserved = stockService.reserve(
        item.getProductId(),
        item.getQuantity()
      );

      if (!reserved) {
        throw new Error(
          `商品「${item.getProductName()}」の在庫が不足しています`
        );
      }
    }

    // ステータス変更
    this.status = OrderStatus.CONFIRMED;

    // ドメインイベント発行(実装は省略)
    // DomainEvents.raise(new OrderConfirmed(this.orderId));
  }

  // ビジネスロジック:合計金額計算
  calculateTotal(member: Member): OrderAmount {
    // 明細の小計を合算
    let total = this.orderItems.reduce(
      (sum, item) => sum + item.getSubtotal(),
      0
    );

    // ゴールド会員は送料無料
    const shippingFee = member.isGoldMember() ? 0 : 500;
    total += shippingFee;

    return OrderAmount.from(total);
  }
}

このコードは、ユビキタス言語を使ったメソッド名(confirm, calculateTotal)、ビジネスルールの実装(在庫確保、会員ランクによる送料計算)、不変条件の保証が含まれています。

生成コードのレビュー

Devin が生成したコードを、ドメインエキスパートと共にレビューします。

markdown# レビューチェックリスト

## 用語の一致

- ✅ Order(注文)、confirm(確定)などビジネス用語を使用
- ✅ ステータスが日本語で表現されている

## ビジネスルールの実装

- ✅ 在庫確保が実装されている
- ✅ ゴールド会員の送料無料が実装されている
- ✅ ステータスが「受付中」の場合のみ確定可能

## 不変条件の保証

- ✅ 注文明細が空の場合エラー
- ✅ 確定済みの注文は再確定不可

## 改善点

- ❌ 仮押さえ期限の実装がない → プロンプトに追加が必要

レビューで見つかった改善点を、次のプロンプトに反映させます。

以下の図は、プロンプトからコード生成、レビューまでの一連のフローを示しています。

mermaidsequenceDiagram
  participant Dev as 開発者
  participant Expert as ドメインエキスパート
  participant Prompt as プロンプト
  participant Devin as Devin
  participant Code as 生成コード

  Dev->>Expert: ドメイン用語のヒアリング
  Expert-->>Dev: ユビキタス言語を共有
  Dev->>Prompt: 用語とルールを記載
  Dev->>Devin: プロンプト投入
  Devin->>Code: コード生成
  Code->>Dev: 生成結果を確認
  Dev->>Expert: コードレビュー依頼
  Expert-->>Dev: ビジネスロジックの妥当性確認
  Dev->>Prompt: フィードバックを反映

このシーケンス図が示すように、開発者とドメインエキスパートが協力し、Devin を介してドメインモデルを構築していくのです。

在庫管理ドメインでの値オブジェクト設計

次に、在庫管理ドメインにおける値オブジェクトの設計を見ていきましょう。値オブジェクトは、ドメインの意味を型で表現する重要なパターンです。

ドメイン用語の定義

在庫管理では、以下のような用語が使われます。

markdown# 在庫管理ドメインのユビキタス言語

- **在庫数(StockQuantity)**:倉庫にある商品の数量
- **安全在庫(SafetyStock)**:欠品を防ぐための最低保持数
- **在庫確保(StockReservation)**:注文確定時の在庫予約
- **在庫補充(StockReplenishment)**:倉庫への商品入荷

これらの用語を値オブジェクトとして実装します。

値オブジェクトのプロンプト設計

markdown# タスク:StockQuantity 値オブジェクトの実装

在庫数を表す値オブジェクトを TypeScript で実装してください。

# 要件

**内部値**

- number 型(整数のみ)

**制約**

- 在庫数は 0 以上
- 負の値は許容しない
- 小数点は許容しない

**不変性**

- 一度作成したら内部値を変更できない
- 演算結果は新しいインスタンスとして返す

# ビジネスロジック

`reserve(quantity: StockQuantity)` メソッド:

- 指定された数量を確保し、残りの在庫数を返す
- 在庫不足の場合はエラーをスロー

`replenish(quantity: StockQuantity)` メソッド:

- 指定された数量を補充し、新しい在庫数を返す

`isBelowSafetyLevel(safetyStock: StockQuantity)` メソッド:

- 安全在庫を下回っているか判定

# 実装パターン

- コンストラクタはプライベート
- `from(value: number)` 静的ファクトリメソッドで生成
- `equals(other: StockQuantity)` で等価性判定

このプロンプトには、値オブジェクトの特性(不変性、等価性)とビジネスロジックが明記されています。

Devin が生成する値オブジェクト

typescript// 値オブジェクト:StockQuantity

class StockQuantity {
  private constructor(private readonly value: number) {
    // バリデーション:0 以上の整数のみ許容
    if (value < 0) {
      throw new Error(
        '在庫数は 0 以上である必要があります'
      );
    }

    if (!Number.isInteger(value)) {
      throw new Error('在庫数は整数である必要があります');
    }
  }

  // ファクトリメソッド
  static from(value: number): StockQuantity {
    return new StockQuantity(value);
  }

  // ゲッター
  getValue(): number {
    return this.value;
  }

  // ビジネスロジック:在庫確保
  reserve(quantity: StockQuantity): StockQuantity {
    const remaining = this.value - quantity.getValue();

    if (remaining < 0) {
      throw new Error(
        `在庫不足です。必要: ${quantity.getValue()}, 現在: ${
          this.value
        }`
      );
    }

    return StockQuantity.from(remaining);
  }

  // ビジネスロジック:在庫補充
  replenish(quantity: StockQuantity): StockQuantity {
    return StockQuantity.from(
      this.value + quantity.getValue()
    );
  }

  // ビジネスロジック:安全在庫チェック
  isBelowSafetyLevel(safetyStock: StockQuantity): boolean {
    return this.value < safetyStock.getValue();
  }

  // 等価性判定
  equals(other: StockQuantity): boolean {
    return this.value === other.getValue();
  }
}

このコードは、ドメインの意味(在庫数)を型として表現し、ビジネスルールをメソッドとして実装しています。

値オブジェクトの活用例

生成された値オブジェクトを、エンティティから利用します。

typescript// エンティティ:Product(商品)

class Product {
  constructor(
    private readonly productId: ProductId,
    private readonly productName: string,
    private stockQuantity: StockQuantity,
    private readonly safetyStock: StockQuantity
  ) {}

  // 在庫確保メソッド
  reserveStock(requestedQuantity: StockQuantity): void {
    // 値オブジェクトのビジネスロジックを利用
    this.stockQuantity = this.stockQuantity.reserve(
      requestedQuantity
    );

    // 安全在庫を下回った場合は警告(ドメインイベント発行)
    if (
      this.stockQuantity.isBelowSafetyLevel(
        this.safetyStock
      )
    ) {
      // DomainEvents.raise(new StockBelowSafetyLevel(this.productId));
      console.warn(
        `商品「${this.productName}」の在庫が安全在庫を下回りました`
      );
    }
  }

  // 在庫補充メソッド
  replenishStock(quantity: StockQuantity): void {
    this.stockQuantity =
      this.stockQuantity.replenish(quantity);
  }

  // 在庫数取得
  getStockQuantity(): StockQuantity {
    return this.stockQuantity;
  }
}

値オブジェクトを使うことで、エンティティのコードがシンプルになり、ビジネスロジックが明確になります。

プリミティブ型を使った場合との比較

値オブジェクトを使わず、プリミティブ型(number)を使った場合と比較してみましょう。

typescript// ❌ プリミティブ型を使った実装

class Product {
  constructor(
    private productId: string,
    private stockQuantity: number, // 単なる数値
    private safetyStock: number
  ) {}

  reserveStock(quantity: number): void {
    // バリデーションがエンティティに散在
    if (quantity < 0) {
      throw new Error('数量は 0 以上である必要があります');
    }

    if (this.stockQuantity - quantity < 0) {
      throw new Error('在庫不足です');
    }

    this.stockQuantity -= quantity;

    // 比較もプリミティブ型で行う
    if (this.stockQuantity < this.safetyStock) {
      console.warn('安全在庫を下回りました');
    }
  }
}

// ✅ 値オブジェクトを使った実装

class Product {
  constructor(
    private productId: ProductId,
    private stockQuantity: StockQuantity, // ドメインの意味を持つ型
    private safetyStock: StockQuantity
  ) {}

  reserveStock(quantity: StockQuantity): void {
    // バリデーションとビジネスロジックは値オブジェクトに委譲
    this.stockQuantity =
      this.stockQuantity.reserve(quantity);

    // ドメイン用語でビジネスロジックを表現
    if (
      this.stockQuantity.isBelowSafetyLevel(
        this.safetyStock
      )
    ) {
      console.warn('安全在庫を下回りました');
    }
  }
}

値オブジェクトを使うことで、ドメインの意味が型として表現され、コードの可読性と保守性が向上します。

下表は、プリミティブ型と値オブジェクトの違いをまとめたものです。

#観点プリミティブ型(number)値オブジェクト(StockQuantity)
1ドメインの意味単なる数値、意味が不明確「在庫数」という意味が明確
2バリデーションエンティティ内に散在値オブジェクト内で集中管理
3ビジネスロジックエンティティに詰め込まれる値オブジェクトに委譲
4不変性保証されないイミュータブル
5型安全性低い(数値なら何でも代入可)高い(専用の型)

このように、値オブジェクトはドメインの意味を型で表現する強力な手段なのです。

リファクタリング時のプロンプト戦略

既存のコードベースを DDD に沿ってリファクタリングする際も、Devin とユビキタス言語を活用できます。

既存コードの分析

まず、リファクタリング対象のコードを Devin に分析させます。

markdown# タスク:既存コードのドメイン用語分析

以下の TypeScript コードを分析し、ドメインドリブン設計の観点から改善点を指摘してください。

# 分析対象コード

```typescript
class Order {
  id: string;
  userId: string;
  items: any[];
  total: number;
  status: string;

  submit() {
    this.status = 'submitted';
    // 処理
  }
}
```

# 分析項目

1. ドメイン用語との不一致を指摘
2. プリミティブ型の乱用を指摘
3. ビジネスロジックの欠落を指摘
4. 改善案を提示

Devin はこのプロンプトに基づき、以下のような分析結果を返します。

markdown# 分析結果

## ドメイン用語との不一致

- `userId` → ビジネス用語では「会員」を使用。`memberId` に変更すべき
- `submit()` → ビジネス用語では「確定」を使用。`confirm()` に変更すべき

## プリミティブ型の乱用

- `id: string``OrderId` 値オブジェクトにすべき
- `total: number``OrderAmount` 値オブジェクトにすべき
- `status: string``OrderStatus` 列挙型にすべき

## ビジネスロジックの欠落

- 在庫確保処理がない
- 金額計算ロジックがない
- 不変条件の保証がない

## 改善案

Order エンティティとして再設計し、ユビキタス言語を反映させる

この分析結果を基に、リファクタリングプロンプトを作成します。

リファクタリングプロンプトの作成

markdown# タスク:Order クラスのリファクタリング

既存の Order クラスをドメイン駆動設計に沿って再設計してください。

# ユビキタス言語

- **注文(Order)**:会員による商品購入行為
- **会員(Member)**:サイト登録済みユーザー
- **注文確定(confirm)**:注文を確定し、在庫確保と決済を実行

# リファクタリング要件

## 値オブジェクトへの置き換え

- `id: string``orderId: OrderId` 値オブジェクト
- `userId: string``memberId: MemberId` 値オブジェクト
- `total: number``total: OrderAmount` 値オブジェクト
- `status: string``status: OrderStatus` 列挙型

## メソッド名の変更

- `submit()``confirm()` に変更

## ビジネスロジックの追加

- `confirm()` メソッドに在庫確保処理を追加
- `calculateTotal()` メソッドを追加

## 不変条件の追加

- コンストラクタで `items` が空でないことを確認
- `confirm()` はステータスが「受付中」の場合のみ実行可能

# 既存コードとの互換性

- 既存の API インターフェースは維持する
- データベースのテーブル構造は変更しない

このプロンプトにより、Devin は既存コードを DDD に沿って段階的にリファクタリングできます。

段階的なリファクタリング

大規模なリファクタリングは、段階的に進めることが重要です。Devin に対しても、ステップごとに指示を出しましょう。

markdown# ステップ 1:値オブジェクトの導入

まず、OrderId 値オブジェクトを導入してください。

- OrderId クラスを作成
- Order クラスの `id: string``orderId: OrderId` に変更
- 既存の文字列 ID を OrderId に変換するアダプターを作成

---

# ステップ 2:列挙型の導入

次に、OrderStatus 列挙型を導入してください。

- OrderStatus 列挙型を作成
- Order クラスの `status: string``status: OrderStatus` に変更
- 既存の文字列ステータスを列挙型に変換

---

# ステップ 3:メソッド名の変更

ビジネス用語に合わせてメソッド名を変更してください。

- `submit()``confirm()` に変更
- 既存の呼び出し箇所をすべて更新

このように段階的に進めることで、既存システムへの影響を最小限に抑えながら、DDD を導入できます。

以下の図は、段階的リファクタリングのプロセスを示しています。

mermaidflowchart LR
  current["現在のコード<br/>(プリミティブ型)"]
  step1["ステップ 1<br/>値オブジェクト導入"]
  step2["ステップ 2<br/>列挙型導入"]
  step3["ステップ 3<br/>メソッド名変更"]
  step4["ステップ 4<br/>ビジネスロジック追加"]
  target["DDD 準拠の<br/>ドメインモデル"]

  current --> step1
  step1 --> step2
  step2 --> step3
  step3 --> step4
  step4 --> target

  step1 -.->|"Devin で自動化"| step2
  step2 -.->|"Devin で自動化"| step3
  step3 -.->|"Devin で自動化"| step4

各ステップで Devin を活用することで、リファクタリングを効率的に進められます。

リファクタリング後の検証

リファクタリングが完了したら、ドメインエキスパートと共にコードをレビューします。

markdown# リファクタリング検証チェックリスト

## ユビキタス言語の反映

- ✅ メソッド名が `confirm()` に変更されている
- ✅ 属性名が `memberId` に変更されている
- ✅ ステータスがビジネス用語で定義されている

## ドメインモデルの品質

- ✅ 値オブジェクトが導入されている
- ✅ 不変条件が保証されている
- ✅ ビジネスロジックがエンティティに実装されている

## 既存システムへの影響

- ✅ API インターフェースが維持されている
- ✅ データベースのマイグレーションが不要
- ✅ 既存のテストが通過している

このように、Devin を活用することで、既存システムを DDD に沿ってリファクタリングできるのです。

まとめ

Devin と進めるドメイン駆動設計において、ユビキタス言語をプロンプトに反映させることは、成功の鍵となります。

本記事では、以下のポイントをご紹介しました。

まず、ドメイン駆動設計の中核概念である「ユビキタス言語」を、Devin とのコミュニケーションに活用する重要性を確認しました。開発者とドメインエキスパートが同じ言葉で会話し、その言葉がコードにまで浸透することで、認識のずれを防ぎ、ビジネスロジックを正確に実装できます。

次に、プロンプトにユビキタス言語が欠如することで起こる問題を明らかにしました。技術用語のみのプロンプトでは、Devin が生成するコードは汎用的すぎて、ドメインの特性を反映しません。ドメインエキスパートとの認識ギャップも、仕様の誤解やバグの原因となるのです。

解決策として、ユビキタス言語を中核としたプロンプト設計をご紹介しました。ドメイン用語の定義、ビジネスルール、エンティティや値オブジェクトの仕様をプロンプトに明記することで、Devin は DDD の原則に沿ったコードを生成できます。さらに、ドメインエキスパートとのレビューサイクルを通じて、継続的に品質を向上させることが可能です。

具体例では、EC サイトの注文ドメインや在庫管理ドメインを通じて、実践的なプロンプト設計とコード生成のプロセスを見てきました。値オブジェクトやエンティティの実装、既存コードのリファクタリングまで、Devin を活用することで、DDD の実践が驚くほど効率的になることをお分かりいただけたでしょう。

AI エージェントと協働する時代において、ユビキタス言語は人間同士のコミュニケーションだけでなく、人間と AI のコミュニケーションにも不可欠な要素です。Devin にドメインの言葉を正確に伝えることで、ビジネス価値の高いソフトウェアを迅速に構築できるのです。

ぜひ、皆さんのプロジェクトでも、ユビキタス言語を反映させたプロンプト設計を試してみてください。ドメインエキスパートと開発チーム、そして Devin が同じ言語で会話することで、真に価値あるシステムが生まれるはずです。

関連リンク