Zenn
Open2

AI時代に必要なプログラミングの基礎とソフトウェア設計を鍛える

上かるび上かるび

TypeScriptで学ぶドメイン駆動設計(DDD)の基礎

DDDの基本概念

1. ユビキタス言語(Ubiquitous Language)

エヴァンスが言うユビキタス言語とは、開発者とドメインエキスパート(ビジネスの専門家)が共通して使う言葉のこと。「ユビキタス」という言葉自体がすでに共通言語になっていない時点で本末転倒な気もするが、要は「みんなで同じ言葉を使おうぜ」ということだ。

以前のプロジェクトでは「それってアレのことよね?」「いやコレだよ」みたいな会話が毎日繰り広げられていた。まるでインド映画の字幕なしバージョンを見ているような混乱状態だった。

自分なりにTypeScriptで書いてみた例:

// 以前私が書いていたコード(何言ってるかわからない系)
interface UserStuff {
  nm: string;
  flg: boolean;
  lvl: string;
}

// ユビキタス言語を意識したコード(人間に優しい系)
interface Member {
  fullName: string;
  hasActiveSubscription: boolean;
  membershipLevel: 'free' | 'standard' | 'premium';
}

変数名nmとか書いてた過去の自分は、母音を買うお金がなかったのかと心配になる。

2. 境界づけられたコンテキスト(Bounded Context)

これは「同じ言葉でも場所によって意味が違うよね」という概念。最初読んだときは「そんなの当たり前じゃん」と思ったが、コードを書くときに意識できていたかと言われると...沈黙は金なり、というやつだ。

例えば「商品」という言葉は:

// 商品表示のコンテキスト(お客さんが見る情報)
namespace CatalogContext {
  interface Product {
    id: string;
    name: string;
    price: number;
    description: string;
    imageUrl: string;
    rating: number; // 現実とは異なる平行世界を示す数値
  }
}

// 在庫管理のコンテキスト(倉庫スタッフが見る情報)
namespace InventoryContext {
  interface Product {
    id: string;
    stockQuantity: number;
    locationCode: string;
    supplier: string;
    lastRestockDate: Date; // 化石の年代測定に使えるほど古い日付
  }
}

以前は「商品データ」として一つのモンスターオブジェクトを作っていた。そして誰も使わないプロパティの墓場と化していた。RESTful APIも大混乱、まさに「REST in Peace API」状態だった。

3. エンティティと値オブジェクト

この概念はかなり理解しやすかった:

  • エンティティ: IDを持つ、変化するもの(人や物)
  • 値オブジェクト: 値自体が重要で、不変なもの(住所やお金)

例えば私は財布の中身が空になっても同じ人間だが、財布の中の1000円札は額面が変われば別のものになる。空の財布と同じく空の心を抱えて生きている。

自分なりに書いてみたコード:

// エンティティの例 - ユーザー
class User {
  readonly id: string; // これが変わると別人
  private name: string;
  private email: string;

  constructor(id: string, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  // 同じIDなら同じユーザー(記憶喪失になっても同一人物)
  equals(other: User): boolean {
    return this.id === other.id;
  }

  // 名前変更メソッド(改名しても同じ人)
  changeName(newName: string): void {
    this.name = newName;
  }
}

// 値オブジェクトの例 - 住所
class Address {
  readonly street: string;
  readonly city: string;
  readonly zipCode: string;
  readonly country: string;

  constructor(street: string, city: string, zipCode: string, country: string) {
    this.street = street;
    this.city = city;
    this.zipCode = zipCode;
    this.country = country;
  }

  // 全部同じなら同じ住所(隣の家は別の住所)
  equals(other: Address): boolean {
    return this.street === other.street &&
           this.city === other.city &&
           this.zipCode === other.zipCode &&
           this.country === other.country;
  }

  // 変更は新オブジェクト(引っ越し)
  withNewStreet(newStreet: string): Address {
    return new Address(newStreet, this.city, this.zipCode, this.country);
  }
}

値オブジェクトは「住所変更」という言葉にだまされてはいけない。実際は「新しい住所」を作って「古い住所」を捨てるのだ。まるで賃貸契約の更新料を払いたくない人のように、毎回新しい家に引っ越すイメージだ。

実際にやってみた: オンライン書店のモデリング

理論だけじゃ頭に入らないので、無謀にも「オンライン書店」をモデリングしてみた。ベゾスに肩を並べる野望はないが、せめて肩を借りるくらいの気概で。

ステップ1: ユビキタス言語を考える

// オンライン書店のユビキタス言語メモ
/*
 * Book: 販売される本(紙の束ではなくデータとしての本)
 * Author: 本の著者(実在の人ではなくシステム上の存在)
 * Customer: お客さん(実在の人ではなく...以下略)
 * Order: 注文(お買い物の契約的な...)
 * 
 * 用語を説明しようとすると「存在とは何か」という哲学的な議論に発展するので止めておこう
 */

この時点で既に疲れた。実際のプロジェクトだと、この用語集は聖書のように分厚くなりそうだ。もしくは「DDDを極めるとあなたもエヴァンスになれる!」的な怪しい自己啓発本になる。

ステップ2: エンティティと値オブジェクトを考える

// エンティティ
class Book {
  readonly isbn: string; // 書籍界のIDカード
  title: string;
  authors: Author[];
  price: Price;
  
  constructor(isbn: string, title: string, authors: Author[], price: Price) {
    this.isbn = isbn;
    this.title = title;
    this.authors = authors;
    this.price = price;
  }
  
  equals(other: Book): boolean {
    return this.isbn === other.isbn; // 中身が全部変わっても同じ本(改訂版という名の別の本)
  }
}

// 値オブジェクト
class Price {
  readonly amount: number;
  readonly currency: string;
  
  constructor(amount: number, currency: string = 'JPY') {
    this.amount = amount;
    this.currency = currency;
  }
  
  equals(other: Price): boolean {
    return this.amount === other.amount && 
           this.currency === other.currency;
  }
  
  add(other: Price): Price {
    if (this.currency !== other.currency) {
      throw new Error('通貨単位が異なる...為替の変動より私の気分の変動の方が激しい');
    }
    return new Price(this.amount + other.amount, this.currency);
  }
}

これを書いていて気づいたのは、今まで私が書いていたコードでは「エンティティ」と「値オブジェクト」の区別がなく、すべてがミュータブル(変更可能)だったこと。メソッドが「副作用のアミューズメントパーク」と化していた。

ステップ3: 境界づけられたコンテキストを定義してみる

// 書籍カタログのコンテキスト(お客さん向け)
namespace CatalogContext {
  interface BookDetails {
    isbn: string;
    title: string;
    authorNames: string[];
    summary: string;
    coverImage: string; // 「見た目で判断するな」という格言を無視する用
    price: Price;
  }
}

// 注文のコンテキスト(会計システム向け)
namespace OrderContext {
  interface CartItem {
    bookIsbn: string;
    quantity: number; // 何かの暗号解読に必要なのか、同じ本を大量購入する謎の存在がいる
    unitPrice: Price;
  }
  
  interface Order {
    id: string;
    customerId: string;
    items: CartItem[];
    shippingAddress: Address;
    orderDate: Date;
    totalAmount: Price; // 銀行口座残高と反比例する数字
  }
}

これで何となく「このコンテキストでは何が重要なのか」が見えてくる。カタログでは「見た目」と「内容」が重要で、注文では「数量」と「金額」が重要、というような。まるで人間関係と同じで、立場によって相手に求めるものが違うということだ。

まとめ

DDDを学び始めたばかりの私だが、エリック・エヴァンスの著書を通じていくつか重要な気づきを得た:

  1. コードは書き手のためではなく、読み手のためにある(そして将来の自分も読み手)
  2. ドメインモデルは技術的な都合ではなく、ビジネスの実態を反映すべき
  3. コンテキストを明確に分けることで、混乱を防げる

特に値オブジェクトの不変性の考え方は目から鱗だった。「変更される値」を追いかけるデバッグ地獄から解放されそうだ。これまでの人生はどこで変数が変更されたかを追跡する探偵ゲームだった。

まだ「集約(Aggregate)」や「リポジトリ」などの概念は理解できていないが、少しずつ学んでいきたい。エヴァンスの本も最初は難解だったが、実装を通じて理解すると面白い。

上かるび上かるび

TypeScriptで効果的なプロンプトエンジニアリング - AI時代のコード生成術

TypeScriptでの実践例

例1: 単純なユーティリティ関数の生成

悪い例:

TypeScriptで配列を操作する関数を書いて

良い例:

【タスク】: TypeScriptで配列から重複を除去する汎用ユーティリティ関数を実装する

【コンテキスト】: 様々な型の配列(数値、文字列、オブジェクト)から重複要素を除去する必要がある

【制約条件】:
- ジェネリック型を使用して、どんな型の配列にも対応できるようにする
- イミュータブルな実装にする(元の配列を変更しない)
- オブジェクトの場合は指定したプロパティに基づいて重複判定できるようにする

【期待する出力】:
- 関数のTypeScript実装
- 使用例を含むサンプルコード
- 簡単な説明

【コード例】:
// 現在のプロジェクトのコーディングスタイル
const add = <T>(a: T, b: T): T[] => {
  return [a, b];
};

このように具体的な指示を出すことで、AIは以下のような質の高いコードを生成しやすくなる:

/**
 * 配列から重複要素を除去する汎用ユーティリティ関数
 * @param array 処理対象の配列
 * @param keySelector オブジェクトの場合、重複判定に使用するキーを選択する関数
 * @returns 重複が除去された新しい配列
 */
export const removeDuplicates = <T>(
  array: readonly T[],
  keySelector?: (item: T) => any
): T[] => {
  if (!keySelector) {
    return Array.from(new Set(array));
  }

  const seen = new Set<any>();
  return array.filter(item => {
    const key = keySelector(item);
    if (seen.has(key)) {
      return false;
    }
    seen.add(key);
    return true;
  });
};

// 使用例
// 基本的な型の配列
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = removeDuplicates(numbers);
// 結果: [1, 2, 3, 4, 5]

// オブジェクトの配列
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 1, name: "Alice (duplicate)" },
  { id: 3, name: "Charlie" }
];
const uniqueUsers = removeDuplicates(users, user => user.id);
// 結果: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Charlie" }]

例2: コンポーネントの実装

Reactコンポーネントを作成する場合にも、構造化されたプロンプトが効果的だ:

【タスク】: TypeScriptとReactで、ページネーション付きのデータテーブルコンポーネントを実装する

【コンテキスト】: ユーザー一覧を表示するダッシュボード画面で使用する。データは非同期APIから取得する。

【制約条件】:
- TypeScriptの型安全性を確保する
- React Hooksを使用する
- レスポンシブデザインに対応する
- アクセシビリティに配慮する
- カスタマイズ可能な列の定義をサポートする

【期待する出力】:
- コンポーネントの実装コード
- 型定義
- 使用例

【コード例】:
// 他のコンポーネントでの実装スタイル
interface ButtonProps {
  onClick?: () => void;
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

const Button: React.FC<ButtonProps> = ({ 
  onClick, 
  children, 
  variant = 'primary' 
}) => {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

このプロンプトから生成されるコードはより詳細で、指定した要件に沿ったものになる可能性が高い。

プロンプトエンジニアリングの高度なテクニック

1. ペルソナの設定

AIに特定のペルソナを演じさせることで、特定の視点やスタイルでコードを生成させることができる:

あなたはTypeScriptとクリーンアーキテクチャの専門家で、常にベストプラクティスに従ったコードを書くシニアエンジニアです。以下の課題に取り組んでください:

【タスク】: ユーザー認証システムのドメインモデルとユースケースを実装する
...

2. フィードバックループの活用

一度生成されたコードに対して、具体的なフィードバックを提供し改善を求める:

生成されたコードについて、以下の点を改善してください:

1. UserServiceクラスの責務が多すぎるので、認証関連の機能をAuthServiceとして分離する
2. エラーハンドリングをより堅牢にする
3. テストがしやすいように依存性を注入できる設計にする

3. ステップバイステップのアプローチ

複雑な実装を一度に求めるのではなく、段階的に構築していく:

【ステップ1】: まず、データの型定義とインターフェースを設計してください
【ステップ2】: 次に、基本的なCRUD操作を行うリポジトリインターフェースを実装してください
【ステップ3】: それらを使用するユースケースクラスを作成してください

TypeScriptプロジェクトに特化したプロンプトのコツ

1. 型定義を先に要求する

TypeScriptの強みは型システムなので、まず型定義から設計させるのが効果的:

【タスク】: オンラインショッピングカートの実装

【ステップ1】: まず、以下の概念の型定義/インターフェースを作成してください:
- Product(商品)
- CartItem(カート内アイテム)
- Cart(ショッピングカート)
- Discount(割引)

【ステップ2】: これらの型を使用して、カートの操作機能を実装してください:
...

2. ユースケースシナリオの提供

具体的なユースケースを示すことで、より実用的なコードを生成できる:

【ユースケースシナリオ】:
1. ユーザーがカートに商品を追加する
2. 商品の数量を変更する
3. 商品をカートから削除する
4. 割引コードを適用する
5. 合計金額を計算する

3. エラーケースの明示

エラーハンドリングは往々にして忘れられがちなので、明示的に要求する:

【エラーケース】:
- 在庫がない商品がカートに追加された場合
- 無効な割引コードが適用された場合
- 数量がゼロ以下に設定された場合

実践課題:プロンプトの作成と評価

前回学んだDDDの概念と今回のプロンプトエンジニアリングを組み合わせて、オンライン書店システムの一部を実装するプロンプトを作成してみよう。

【タスク】: オンライン書店の書籍検索機能をTypeScriptで実装する

【コンテキスト】: DDDアプローチに基づく「書籍カタログ」の境界づけられたコンテキスト内での実装

【ドメインモデル】:
- Book(エンティティ): ISBN、タイトル、著者リスト、説明、価格を持つ
- Author(値オブジェクト): 名前、略歴を持つ
- Price(値オブジェクト): 金額と通貨単位を持つ

【制約条件】:
- クリーンアーキテクチャに従う(ドメイン、ユースケース、インフラストラクチャの層を分ける)
- イミュータブルなドメインモデル
- 検索は複数の条件(タイトル、著者名、価格帯)をサポートする
- 非同期処理を適切に扱う

【期待する出力】:
- ドメインモデルの型定義
- リポジトリインターフェース
- 検索ユースケースの実装
- インメモリ実装のリポジトリ(テスト用)
- 使用例

まとめ

効果的なプロンプトエンジニアリングは、AIとの協業における重要なスキルだ。特にTypeScriptのような静的型付け言語では、型定義や構造を明確に伝えることで、より高品質なコードを生成できる。

今回学んだポイント:

  1. 構造化されたプロンプトテンプレートを活用する
  2. タスク、コンテキスト、制約条件、期待する出力を明確にする
  3. 型定義を先に要求する
  4. ユースケースシナリオやエラーケースを明示する
  5. フィードバックループを活用して段階的に改善する
ログインするとコメントできます