Skip to content

Instantly share code, notes, and snippets.

@haru01
Last active June 12, 2025 09:59
Show Gist options
  • Select an option

  • Save haru01/dd2c2b216297c0a639789658e77598a7 to your computer and use it in GitHub Desktop.

Select an option

Save haru01/dd2c2b216297c0a639789658e77598a7 to your computer and use it in GitHub Desktop.
関数型DDD学習用のClaudeファイル

TypeScript関数型DDD実践ガイド(AI最適化版)

プロジェクト概要

プロジェクト説明

関数型プログラミングとDomain-Driven Designの原則を組み合わせたTypeScript実装ガイド。fp-tsライブラリを使用し、純粋関数、不変性、型安全性を重視したアーキテクチャを提供します。

  • 純粋なドメイン層: 副作用なしの純粋関数のみ
  • 型安全性: ブランド型とZodによる実行時検証
  • 関数合成: TaskEitherパターンによるエラーハンドリング
  • 不変性: 全データ構造の不変性保証

開発コマンド

npm run type-check        # 型チェック(厳密モード)
npm run test              # ドメインロジックテスト実行
npm run lint:functional   # 関数型パターンのリント
npm run example           # 実行可能サンプル
npm run build             # 本番ビルド

セットアップと依存関係

package.json

{
  "name": "typescript-functional-ddd",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc --strict",
    "test": "vitest run",
    "test:watch": "vitest",
    "type-check": "tsc --noEmit --strict",
    "lint:functional": "eslint --config .eslintrc.functional.js",
    "example": "tsx src/example.ts"
  },
  "dependencies": {
    "fp-ts": "^2.16.9",
    "zod": "^3.22.4",
    "uuid": "^9.0.1"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "@types/uuid": "^9.0.7",
    "typescript": "^5.3.0",
    "tsx": "^4.6.0",
    "vitest": "^1.0.0"
  }
}

プロジェクト構造

src/
├── shared/
│   ├── types.ts          # 共通型定義とエラー型
│   └── functional.ts     # 関数型ユーティリティ
└── order/
    ├── domain/
    │   ├── types.ts      # Zodスキーマ + 状態遷移定義
    │   └── functions.ts  # 純粋ドメイン関数
    ├── application/
    │   └── service.ts    # TaskEitherサービス層
    └── infrastructure/
        └── repository.ts # 永続化実装

ドメイン概念とパターン

値オブジェクト(Value Objects)

/**
 * @concept 値オブジェクト - アイデンティティを持たないドメインオブジェクト
 * @reasoning 属性によって定義され、不変性を保証
 * @invariants 作成後は変更不可、等価性は値で判定
 * Value Objectsの実装はZodのスキーマで実現する
 */

エンティティ(Entities)

/**
 * @concept エンティティ - アイデンティティを持つドメインオブジェクト
 * @reasoning IDによる同一性、ライフサイクルを持つ
 * @invariants IDは不変、状態変更は明示的メソッドのみ
 * Entityの実装はZodのスキーマで実現する
 */

集約(Aggregates)

/**
 * @concept 集約 - 整合性境界を持つエンティティ群
 * @reasoning ビジネス不変条件の境界、トランザクション境界
 * @invariants 外部からはルートエンティティ経由でのみアクセス
 */

状態遷移システム(Zod + 代数データ型)

核心アプローチ

/**
 * @reasoning 状態遷移システムの設計原則
 * 1. 代数データ型: discriminatedUnionで状態ごとの必要フィールドを明確化
 * 2. 設定駆動: 状態遷移ルールをマップで管理
 * 3. アクションビルダー: 型安全なアクション作成
 * 4. 不正状態の排除: optionalフィールドを使わず型で整合性保証
 */

// 基本スキーマ定義
const MoneySchema = z.object({
  amount: z.number().nonnegative().describe('非負の金額'),
  currency: z.string().regex(/^[A-Z]{3}$/).describe('ISO 4217通貨コード')
});

const AddressSchema = z.object({
  street: z.string().min(1).describe('住所(番地)'),
  city: z.string().min(1).describe('市区町村'),
  zipCode: z.string().regex(/^\d{3}-\d{4}$/).describe('郵便番号(XXX-XXXX形式)')
});

const OrderItemSchema = z.object({
  productId: z.string().min(1).describe('商品ID'),
  quantity: z.number().int().positive().max(100).describe('数量(1-100)'),
  unitPrice: MoneySchema.describe('単価')
});

代数データ型による状態管理

/**
 * @description 注文の状態別スキーマ定義
 * @reasoning 各状態で必要なフィールドのみを定義、不正状態を型で排除
 * @verification
 * - Q: なぜoptionalフィールドを使わないのか?
 *   A: 状態によって必須/不要なフィールドが明確になり、型安全性が向上
 * - Q: 新しい状態を追加するには?
 *   A: discriminatedUnionに新しい状態スキーマを追加
 */
const BaseOrderSchema = z.object({
  id: z.string().uuid().describe('注文ID'),
  customerId: z.string().uuid().describe('顧客ID'),
  items: z.array(OrderItemSchema).min(1).max(10).describe('注文商品リスト'),
  totalAmount: MoneySchema.describe('合計金額'),
  createdAt: z.date().describe('作成日時'),
  updatedAt: z.date().describe('更新日時')
});

export const OrderSchema = z.discriminatedUnion('status', [
  // ドラフト状態(作成直後)
  BaseOrderSchema.extend({
    status: z.literal('draft').describe('ドラフト状態')
  }),

  // 確定状態(決済・配送先確定済み)
  BaseOrderSchema.extend({
    status: z.literal('confirmed').describe('確定状態'),
    paymentId: z.string().describe('決済ID'),
    shippingAddress: AddressSchema.describe('配送先住所'),
    confirmedAt: z.date().describe('確定日時')
  }),

  // 配送中状態
  BaseOrderSchema.extend({
    status: z.literal('shipped').describe('配送中状態'),
    paymentId: z.string().describe('決済ID'),
    shippingAddress: AddressSchema.describe('配送先住所'),
    confirmedAt: z.date().describe('確定日時'),
    trackingNumber: z.string().describe('追跡番号'),
    shippedAt: z.date().describe('発送日時')
  }),

  // キャンセル状態
  BaseOrderSchema.extend({
    status: z.literal('cancelled').describe('キャンセル状態'),
    cancelReason: z.enum(['customer_request', 'payment_failed', 'out_of_stock']).describe('キャンセル理由'),
    cancelDetails: z.string().describe('キャンセル詳細'),
    cancelledAt: z.date().describe('キャンセル日時'),
    refundAmount: z.number().nonnegative().describe('返金額')
  })
]);

アクションとトランジション

/**
 * @description 状態遷移アクションの定義
 * @reasoning 型安全なアクション作成、バリデーション自動化
 */
const ConfirmActionSchema = z.object({
  type: z.literal('CONFIRM'),
  payload: z.object({
    paymentId: z.string().describe('決済システムからのID'),
    shippingAddress: AddressSchema.describe('配送先住所')
  })
});

const ShipActionSchema = z.object({
  type: z.literal('SHIP'),
  payload: z.object({
    trackingNumber: z.string().describe('配送業者の追跡番号')
  })
});

const CancelActionSchema = z.object({
  type: z.literal('CANCEL'),
  payload: z.object({
    reason: z.enum(['customer_request', 'payment_failed', 'out_of_stock']).describe('キャンセル理由'),
    details: z.string().min(5).describe('詳細説明(5文字以上)'),
    refundAmount: z.number().nonnegative().describe('返金額')
  })
});

export const OrderActionSchema = z.union([ConfirmActionSchema, ShipActionSchema, CancelActionSchema]);

// 型推論
export type Order = z.infer<typeof OrderSchema>;
export type OrderAction = z.infer<typeof OrderActionSchema>;

/**
 * @description 状態遷移マップ(設定駆動)
 * @reasoning 遷移ルールの一元管理、新しい状態・遷移の追加容易性
 */
export const TRANSITION_MAP = {
  draft: { CONFIRM: 'confirmed', CANCEL: 'cancelled' },
  confirmed: { SHIP: 'shipped', CANCEL: 'cancelled' },
  shipped: { CANCEL: 'cancelled' },
  cancelled: {}
} as const;

純粋ドメイン関数

注文作成関数

/**
 * @description 注文集約の作成
 * @reasoning
 * 1. 入力の検証(顧客ID、商品リスト)
 * 2. 合計金額の計算(ビジネスルール適用)
 * 3. 初期状態(draft)での注文オブジェクト生成
 * 4. Zodスキーマによる最終検証
 * @param customerId 顧客ID(UUID形式)
 * @param items 注文商品リスト(1-10個)
 * @returns Result<Order> 成功時は注文オブジェクト、失敗時はエラーメッセージ
 * @example
 * ```typescript
 * import { createOrder } from './domain/functions';
 *
 * const items: OrderItem[] = [{
 *   productId: 'laptop-001',
 *   quantity: 1,
 *   unitPrice: { amount: 98000, currency: 'JPY' }
 * }];
 *
 * const result = createOrder('customer-uuid' as CustomerId, items);
 * if (result.success) {
 *   console.log('注文作成成功:', result.value.id);
 * } else {
 *   console.error('注文作成失敗:', result.error);
 * }
 * ```
 */
export const createOrder = (
  customerId: CustomerId,
  items: readonly OrderItem[]
): Result<Order> => {
  const now = new Date();

  // ビジネスルール: 合計金額計算
  const totalAmount = {
    amount: items.reduce((sum, item) => sum + (item.quantity * item.unitPrice.amount), 0),
    currency: items[0]?.unitPrice.currency || 'JPY'
  };

  // 注文オブジェクト構築
  const order = {
    id: createOrderId(),
    customerId,
    items,
    status: 'draft' as const,
    totalAmount,
    createdAt: now,
    updatedAt: now
  };

  // Zodスキーマによる検証
  const result = OrderSchema.safeParse(order);
  return result.success
    ? { success: true, value: result.data }
    : { success: false, error: result.error.errors[0]?.message || '不正な注文データ' };
};

状態遷移関数

/**
 * @description 注文状態の遷移処理
 * @reasoning
 * 1. アクションの形式検証(Zodスキーマ)
 * 2. 現在状態からの遷移可能性チェック(TRANSITION_MAP)
 * 3. 新しい状態の構築(代数データ型)
 * 4. 最終的な状態の検証(Zodスキーマ)
 * @param order 現在の注文状態
 * @param action 実行するアクション
 * @returns Result<Order> 遷移後の注文状態または エラー
 * @verification
 * - Q: 不正な遷移を実行したらどうなる?
 *   A: TRANSITION_MAPでチェックし、不正な場合はエラーを返す
 * - Q: 部分的なデータ更新は可能?
 *   A: 不可。新しい状態を完全に構築し、Zodで検証する
 * @example
 * ```typescript
 * const confirmAction = Actions.confirm('PAY-123', {
 *   street: '1-1-1 Shibuya',
 *   city: 'Tokyo',
 *   zipCode: '150-0002'
 * });
 *
 * const result = transition(draftOrder, confirmAction);
 * ```
 */
export const transition = (order: Order, action: OrderAction): Result<Order> => {
  // 1. アクション検証
  const actionResult = OrderActionSchema.safeParse(action);
  if (!actionResult.success) {
    return { success: false, error: `不正なアクション: ${actionResult.error.message}` };
  }

  // 2. 遷移可能性チェック
  const allowedActions = TRANSITION_MAP[order.status];
  const newStatus = allowedActions[action.type as keyof typeof allowedActions];
  if (!newStatus) {
    return {
      success: false,
      error: `状態 '${order.status}' からアクション '${action.type}' は実行できません`
    };
  }

  // 3. 新しい状態構築(代数データ型パターン)
  let updatedOrder: Order;

  switch (action.type) {
    case 'CONFIRM':
      updatedOrder = {
        ...order,
        status: 'confirmed' as const,
        paymentId: action.payload.paymentId,
        shippingAddress: action.payload.shippingAddress,
        confirmedAt: new Date(),
        updatedAt: new Date()
      } as Order;
      break;

    case 'SHIP':
      if (order.status !== 'confirmed') {
        return { success: false, error: '確定済み注文のみ発送可能' };
      }
      updatedOrder = {
        ...order,
        status: 'shipped' as const,
        trackingNumber: action.payload.trackingNumber,
        shippedAt: new Date(),
        updatedAt: new Date()
      } as Order;
      break;

    case 'CANCEL':
      updatedOrder = {
        id: order.id,
        customerId: order.customerId,
        items: order.items,
        totalAmount: order.totalAmount,
        createdAt: order.createdAt,
        status: 'cancelled' as const,
        cancelReason: action.payload.reason,
        cancelDetails: action.payload.details,
        cancelledAt: new Date(),
        refundAmount: action.payload.refundAmount,
        updatedAt: new Date()
      } as Order;
      break;

    default:
      return { success: false, error: '未知のアクションタイプ' };
  }

  // 4. 最終検証
  const result = OrderSchema.safeParse(updatedOrder);
  return result.success
    ? { success: true, value: result.data }
    : { success: false, error: `不正な注文状態: ${result.error.message}` };
};

アクションビルダー

/**
 * @description 型安全なアクション作成ヘルパー
 * @reasoning 手動でのアクション作成時のタイポ、型エラーを防止
 */
export const Actions = {
  /**
   * @description 注文確定アクション作成
   * @param paymentId 決済システムからの決済ID
   * @param address 配送先住所
   */
  confirm: (paymentId: string, address: Address): OrderAction => ({
    type: 'CONFIRM',
    payload: { paymentId, shippingAddress: address }
  }),

  /**
   * @description 発送開始アクション作成
   * @param trackingNumber 配送業者の追跡番号
   */
  ship: (trackingNumber: string): OrderAction => ({
    type: 'SHIP',
    payload: { trackingNumber }
  }),

  /**
   * @description キャンセルアクション作成
   * @param reason キャンセル理由(列挙型)
   * @param details 詳細説明(5文字以上)
   * @param refundAmount 返金額(非負数)
   */
  cancel: (
    reason: 'customer_request' | 'payment_failed' | 'out_of_stock',
    details: string,
    refundAmount: number
  ): OrderAction => ({
    type: 'CANCEL',
    payload: { reason, details, refundAmount }
  })
};

ユーティリティ関数

/**
 * @description 注文に関するユーティリティ関数群
 */
export const OrderUtils = {
  /**
   * @description 指定アクションが実行可能かチェック
   * @param order 現在の注文状態
   * @param actionType チェック対象のアクションタイプ
   * @returns boolean 実行可能な場合true
   */
  canTransition: (order: Order, actionType: string): boolean =>
    actionType in (TRANSITION_MAP[order.status] || {}),

  /**
   * @description 終端状態(それ以上遷移できない状態)かチェック
   * @param order チェック対象の注文
   * @returns boolean 終端状態の場合true
   */
  isTerminalState: (order: Order): boolean =>
    Object.keys(TRANSITION_MAP[order.status] || {}).length === 0,

  /**
   * @description 現在状態から実行可能なアクション一覧取得
   * @param order 対象の注文
   * @returns string[] 実行可能なアクション名の配列
   */
  getAvailableActions: (order: Order): string[] =>
    Object.keys(TRANSITION_MAP[order.status] || {})
};

アプリケーションサービス(TaskEither パターン)

エラー型定義

/**
 * @description アプリケーション層のエラー型
 * @reasoning 判別可能ユニオンでエラーハンドリングを型安全に
 */
export type AppError =
  | { type: 'ValidationError'; message: string; details?: unknown }
  | { type: 'DomainError'; message: string; context?: Record<string, unknown> }
  | { type: 'TransitionError'; message: string; currentState?: string }
  | { type: 'NotFoundError'; resource: string; id: string }
  | { type: 'ConcurrencyError'; message: string; version?: number };

/**
 * @description エラー作成ヘルパー関数
 */
export const validationError = (message: string, details?: unknown): AppError =>
  ({ type: 'ValidationError', message, details });

export const domainError = (message: string, context?: Record<string, unknown>): AppError =>
  ({ type: 'DomainError', message, context });

export const transitionError = (message: string, currentState?: string): AppError =>
  ({ type: 'TransitionError', message, currentState });

export const notFoundError = (resource: string, id: string): AppError =>
  ({ type: 'NotFoundError', resource, id });

/**
 * @description TaskEither型のエイリアス
 * @reasoning アプリケーション層での標準的な戻り値型
 */
export type AppTaskEither<T> = TaskEither<AppError, T>;

コマンドとクエリの定義

/**
 * @description 注文作成コマンド
 * @reasoning CQRSパターン、コマンドとクエリの分離
 */
export const CreateOrderCommandSchema = z.object({
  customerId: z.string().uuid().describe('顧客ID'),
  items: z.array(z.object({
    productId: z.string().min(1).describe('商品ID'),
    quantity: z.number().int().positive().max(100).describe('注文数量'),
    unitPrice: z.number().nonnegative().describe('単価'),
    currency: z.string().regex(/^[A-Z]{3}$/).describe('通貨コード')
  })).min(1).max(10).describe('注文商品リスト')
});

export type CreateOrderCommand = z.infer<typeof CreateOrderCommandSchema>;

/**
 * @description 状態遷移コマンド
 */
export const TransitionOrderCommandSchema = z.object({
  orderId: z.string().uuid().describe('注文ID'),
  action: OrderActionSchema.describe('実行するアクション')
});

export type TransitionOrderCommand = z.infer<typeof TransitionOrderCommandSchema>;

リポジトリインターフェース

/**
 * @description 注文リポジトリインターフェース
 * @reasoning インフラストラクチャ層への依存性の抽象化
 */
export interface OrderRepository {
  /**
   * @description 注文の保存
   * @param order 保存対象の注文
   * @returns AppTaskEither<Order> 保存後の注文(楽観的ロック用バージョン更新済み)
   */
  save(order: Order): AppTaskEither<Order>;

  /**
   * @description ID による注文検索
   * @param orderId 検索対象の注文ID
   * @returns AppTaskEither<Order> 見つかった注文
   * @errors NotFoundError 注文が存在しない場合
   */
  findById(orderId: string): AppTaskEither<Order>;

  /**
   * @description 顧客ID による注文一覧取得
   * @param customerId 検索対象の顧客ID
   * @returns AppTaskEither<readonly Order[]> 該当する注文の配列
   */
  findByCustomerId(customerId: CustomerId): AppTaskEither<readonly Order[]>;

  /**
   * @description 注文の存在確認
   * @param orderId 確認対象の注文ID
   * @returns AppTaskEither<boolean> 存在する場合true
   */
  exists(orderId: string): AppTaskEither<boolean>;
}

ハンドラー実装

/**
 * @description 注文作成ハンドラー
 * @reasoning TaskEitherモナドによる関数合成、エラーハンドリング
 * @param repository 注文リポジトリ
 * @returns ハンドラー関数
 * @example
 * ```typescript
 * import { pipe } from 'fp-ts/function';
 * import { fold } from 'fp-ts/TaskEither';
 *
 * const handler = createOrderHandler(repository);
 * const command = {
 *   customerId: 'customer-uuid',
 *   items: [{ productId: 'item-1', quantity: 1, unitPrice: 1000, currency: 'JPY' }]
 * };
 *
 * const result = await handler(command)();
 * pipe(
 *   result,
 *   E.fold(
 *     error => console.error('エラー:', error),
 *     order => console.log('作成成功:', order.id)
 *   )
 * );
 * ```
 */
export const createOrderHandler = (repository: OrderRepository) => {
  return (command: CreateOrderCommand): AppTaskEither<Order> => {

    /**
     * @step 1. コマンド検証
     */
    const validateCommand = (): Either<AppError, CreateOrderCommand> => {
      const result = CreateOrderCommandSchema.safeParse(command);
      return result.success
        ? right(command)
        : left(validationError('不正なコマンド', result.error.errors));
    };

    /**
     * @step 2. ドメインオブジェクト構築
     */
    const buildOrderItems = (): Either<AppError, OrderItem[]> => {
      try {
        const items: OrderItem[] = command.items.map(item => ({
          productId: item.productId,
          quantity: item.quantity,
          unitPrice: { amount: item.unitPrice, currency: item.currency }
        }));
        return right(items);
      } catch (error) {
        return left(domainError('商品リスト構築エラー', { error }));
      }
    };

    /**
     * @step 3. 注文作成(ドメインロジック)
     */
    const buildOrder = (items: OrderItem[]): Either<AppError, Order> => {
      const result = createOrder(command.customerId as CustomerId, items);
      return result.success
        ? right(result.value)
        : left(domainError(result.error));
    };

    /**
     * @step 4. TaskEitherパイプライン実行
     */
    return pipe(
      fromEither(validateCommand()),
      chain(() => fromEither(buildOrderItems())),
      chain(items => fromEither(buildOrder(items))),
      chain(order => repository.save(order))
    );
  };
};

/**
 * @description 注文状態遷移ハンドラー
 * @reasoning 状態遷移ロジックの統一的な処理
 * @param repository 注文リポジトリ
 * @returns ハンドラー関数
 */
export const transitionOrderHandler = (repository: OrderRepository) => {
  return (orderId: string, action: OrderAction): AppTaskEither<Order> => {

    /**
     * @step 1. 現在の注文状態取得
     */
    const getCurrentOrder = (): AppTaskEither<Order> =>
      repository.findById(orderId);

    /**
     * @step 2. 状態遷移適用(ドメインロジック)
     */
    const applyTransition = (order: Order): Either<AppError, Order> => {
      const result = transition(order, action);
      return result.success
        ? right(result.value)
        : left(transitionError(result.error, order.status));
    };

    /**
     * @step 3. 更新後の注文保存
     */
    return pipe(
      getCurrentOrder(),
      chain(order => fromEither(applyTransition(order))),
      chain(updatedOrder => repository.save(updatedOrder))
    );
  };
};

インフラストラクチャ実装

インメモリリポジトリ

/**
 * @description インメモリ注文リポジトリ実装
 * @reasoning テスト用、プロトタイプ用の軽量実装
 * @usage 本番環境では PostgreSQL/MongoDB 等の実装に置き換え
 */
export class InMemoryOrderRepository implements OrderRepository {
  private orders: Map<string, Order> = new Map();
  private versionCounter = 0;

  /**
   * @description 注文の保存
   * @reasoning 楽観的ロック用のバージョン管理
   */
  save(order: Order): AppTaskEither<Order> {
    try {
      // バージョン管理(楽観的ロック)
      const savedOrder = {
        ...order,
        updatedAt: new Date(),
        version: ++this.versionCounter
      } as Order;

      this.orders.set(order.id, savedOrder);
      return fromEither(right(savedOrder));
    } catch (error) {
      return fromEither(left(domainError('注文保存エラー', { error })));
    }
  }

  /**
   * @description ID による注文検索
   */
  findById(orderId: string): AppTaskEither<Order> {
    const order = this.orders.get(orderId);
    return order
      ? fromEither(right(order))
      : fromEither(left(notFoundError('Order', orderId)));
  }

  /**
   * @description 顧客ID による注文一覧取得
   */
  findByCustomerId(customerId: CustomerId): AppTaskEither<readonly Order[]> {
    const orders = Array.from(this.orders.values())
      .filter(order => order.customerId === customerId);
    return fromEither(right(orders));
  }

  /**
   * @description 注文の存在確認
   */
  exists(orderId: string): AppTaskEither<boolean> {
    return fromEither(right(this.orders.has(orderId)));
  }

  /**
   * @description テスト用: データクリア
   */
  clear(): void {
    this.orders.clear();
    this.versionCounter = 0;
  }

  /**
   * @description テスト用: 全注文取得
   */
  getAll(): readonly Order[] {
    return Array.from(this.orders.values());
  }
}

PostgreSQL リポジトリ例

/**
 * @description PostgreSQL 注文リポジトリ実装例
 * @reasoning 本番環境での永続化実装
 * @requires pg, @types/pg
 */
export class PostgreSQLOrderRepository implements OrderRepository {
  constructor(private pool: Pool) {}

  save(order: Order): AppTaskEither<Order> {
    return pipe(
      TaskEither.tryCatch(
        async () => {
          const client = await this.pool.connect();
          try {
            await client.query('BEGIN');

            // 注文基本情報の保存
            const orderQuery = `
              INSERT INTO orders (id, customer_id, status, total_amount, total_currency, created_at, updated_at)
              VALUES ($1, $2, $3, $4, $5, $6, $7)
              ON CONFLICT (id) DO UPDATE SET
                status = EXCLUDED.status,
                updated_at = EXCLUDED.updated_at
              RETURNING *
            `;

            await client.query(orderQuery, [
              order.id,
              order.customerId,
              order.status,
              order.totalAmount.amount,
              order.totalAmount.currency,
              order.createdAt,
              order.updatedAt
            ]);

            // 注文商品の保存
            await this.saveOrderItems(client, order);

            // 状態固有データの保存
            await this.saveStatusSpecificData(client, order);

            await client.query('COMMIT');
            return order;
          } catch (error) {
            await client.query('ROLLBACK');
            throw error;
          } finally {
            client.release();
          }
        },
        (error) => domainError('注文保存エラー', { error })
      )
    );
  }

  findById(orderId: string): AppTaskEither<Order> {
    return pipe(
      TaskEither.tryCatch(
        async () => {
          const client = await this.pool.connect();
          try {
            const result = await client.query(
              'SELECT * FROM orders WHERE id = $1',
              [orderId]
            );

            if (result.rows.length === 0) {
              throw new Error('Order not found');
            }

            const orderData = result.rows[0];
            return await this.buildOrderFromData(client, orderData);
          } finally {
            client.release();
          }
        },
        (error) => error.message === 'Order not found'
          ? notFoundError('Order', orderId)
          : domainError('注文検索エラー', { error })
      )
    );
  }

  /**
   * @description 注文商品の保存
   * @private
   */
  private async saveOrderItems(client: PoolClient, order: Order): Promise<void> {
    // 既存商品削除
    await client.query('DELETE FROM order_items WHERE order_id = $1', [order.id]);

    // 新規商品挿入
    for (const item of order.items) {
      await client.query(`
        INSERT INTO order_items (order_id, product_id, quantity, unit_price, currency)
        VALUES ($1, $2, $3, $4, $5)
      `, [order.id, item.productId, item.quantity, item.unitPrice.amount, item.unitPrice.currency]);
    }
  }
}

実行例とテストパターン

完全な実行例

/**
 * @example 注文作成から配送までの完全フロー
 * @description 実際のアプリケーションでの使用パターン
 */
import { pipe } from 'fp-ts/function';
import { fold } from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';

const runCompleteOrderFlow = async () => {
  // 依存関係のセットアップ
  const repository = new InMemoryOrderRepository();
  const createHandler = createOrderHandler(repository);
  const transitionHandler = transitionOrderHandler(repository);

  console.log('=== 注文作成から配送完了までのフロー ===\n');

  // 1. 注文作成
  const createCommand: CreateOrderCommand = {
    customerId: '550e8400-e29b-41d4-a716-446655440000',
    items: [
      {
        productId: 'laptop-001',
        quantity: 1,
        unitPrice: 98000,
        currency: 'JPY'
      },
      {
        productId: 'mouse-002',
        quantity: 2,
        unitPrice: 2500,
        currency: 'JPY'
      }
    ]
  };

  const createResult = await createHandler(createCommand)();

  const order = await pipe(
    createResult,
    fold(
      async (error) => {
        console.error('❌ 注文作成エラー:', error);
        throw new Error('注文作成失敗');
      },
      async (order) => {
        console.log(`✅ 注文作成成功: ${order.id}`);
        console.log(`   状態: ${order.status}`);
        console.log(`   合計: ${order.totalAmount.amount} ${order.totalAmount.currency}\n`);
        return order;
      }
    )
  )();

  // 2. 注文確定
  const confirmAction = Actions.confirm('PAY-123456', {
    street: '1-1-1 Shibuya',
    city: 'Tokyo',
    zipCode: '150-0002'
  });

  const confirmResult = await transitionHandler(order.id, confirmAction)();

  const confirmedOrder = await pipe(
    confirmResult,
    fold(
      async (error) => {
        console.error('❌ 注文確定エラー:', error);
        throw new Error('注文確定失敗');
      },
      async (confirmedOrder) => {
        console.log(`✅ 注文確定成功: ${confirmedOrder.id}`);
        console.log(`   状態: ${confirmedOrder.status}`);
        if (confirmedOrder.status === 'confirmed') {
          console.log(`   決済ID: ${confirmedOrder.paymentId}`);
          console.log(`   配送先: ${confirmedOrder.shippingAddress.street}, ${confirmedOrder.shippingAddress.city}\n`);
        }
        return confirmedOrder;
      }
    )
  )();

  // 3. 発送開始
  const shipAction = Actions.ship('TRACK-789012');
  const shipResult = await transitionHandler(confirmedOrder.id, shipAction)();

  await pipe(
    shipResult,
    fold(
      async (error) => {
        console.error('❌ 発送エラー:', error);
      },
      async (shippedOrder) => {
        console.log(`✅ 発送開始: ${shippedOrder.id}`);
        console.log(`   状態: ${shippedOrder.status}`);
        if (shippedOrder.status === 'shipped') {
          console.log(`   追跡番号: ${shippedOrder.trackingNumber}`);
          console.log(`   発送日時: ${shippedOrder.shippedAt?.toISOString()}\n`);
        }
      }
    )
  )();

  // 4. 利用可能アクションの確認
  const finalOrder = repository.findById(order.id);
  await pipe(
    finalOrder,
    fold(
      async () => {},
      async (order) => {
        const availableActions = OrderUtils.getAvailableActions(order);
        console.log(`📋 利用可能なアクション: ${availableActions.join(', ')}`);
        console.log(`🔚 終端状態: ${OrderUtils.isTerminalState(order) ? 'はい' : 'いいえ'}`);
      }
    )
  )();
};

// 実行
runCompleteOrderFlow().catch(console.error);

テストパターン

/**
 * @description ドメイン関数のテストパターン
 * @testing-approach 純粋関数のため、入力と出力のテストが容易
 */
import { describe, it, expect, beforeEach } from 'vitest';

describe('Order Domain Functions', () => {
  describe('createOrder', () => {
    it('正常な注文を作成できる', () => {
      // Arrange
      const customerId = '123e4567-e89b-12d3-a456-426614174000' as CustomerId;
      const items: OrderItem[] = [{
        productId: 'item-1',
        quantity: 2,
        unitPrice: { amount: 1000, currency: 'JPY' }
      }];

      // Act
      const result = createOrder(customerId, items);

      // Assert
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.value.customerId).toBe(customerId);
        expect(result.value.status).toBe('draft');
        expect(result.value.totalAmount.amount).toBe(2000);
        expect(result.value.items).toHaveLength(1);
      }
    });

    it('空の商品リストはエラーを返す', () => {
      // Arrange
      const customerId = '123e4567-e89b-12d3-a456-426614174000' as CustomerId;
      const items: OrderItem[] = [];

      // Act
      const result = createOrder(customerId, items);

      // Assert
      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error).toContain('items');
      }
    });

    it('不正な数量はエラーを返す', () => {
      // Arrange
      const customerId = '123e4567-e89b-12d3-a456-426614174000' as CustomerId;
      const items: OrderItem[] = [{
        productId: 'item-1',
        quantity: 0, // 不正な数量
        unitPrice: { amount: 1000, currency: 'JPY' }
      }];

      // Act
      const result = createOrder(customerId, items);

      // Assert
      expect(result.success).toBe(false);
    });
  });

  describe('transition', () => {
    let draftOrder: Order;

    beforeEach(() => {
      const createResult = createOrder(
        '123e4567-e89b-12d3-a456-426614174000' as CustomerId,
        [{
          productId: 'item-1',
          quantity: 1,
          unitPrice: { amount: 1000, currency: 'JPY' }
        }]
      );

      if (createResult.success) {
        draftOrder = createResult.value;
      } else {
        throw new Error('テスト用注文作成失敗');
      }
    });

    it('draft から confirmed への遷移が成功する', () => {
      // Arrange
      const confirmAction = Actions.confirm('PAY-123', {
        street: '1-1-1 Test',
        city: 'Tokyo',
        zipCode: '100-0001'
      });

      // Act
      const result = transition(draftOrder, confirmAction);

      // Assert
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.value.status).toBe('confirmed');
        expect(result.value.paymentId).toBe('PAY-123');
      }
    });

    it('不正な遷移はエラーを返す', () => {
      // Arrange
      const shipAction = Actions.ship('TRACK-123');

      // Act
      const result = transition(draftOrder, shipAction);

      // Assert
      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error).toContain('SHIP');
        expect(result.error).toContain('draft');
      }
    });
  });
});

/**
 * @description アプリケーションサービスのテストパターン
 * @testing-approach モックリポジトリを使用した統合テスト
 */
describe('Order Application Services', () => {
  let mockRepository: jest.Mocked<OrderRepository>;
  let createHandler: ReturnType<typeof createOrderHandler>;

  beforeEach(() => {
    mockRepository = {
      save: jest.fn(),
      findById: jest.fn(),
      findByCustomerId: jest.fn(),
      exists: jest.fn()
    };

    createHandler = createOrderHandler(mockRepository);
  });

  it('正常な注文作成フロー', async () => {
    // Arrange
    const command: CreateOrderCommand = {
      customerId: '123e4567-e89b-12d3-a456-426614174000',
      items: [{
        productId: 'item-1',
        quantity: 1,
        unitPrice: 1000,
        currency: 'JPY'
      }]
    };

    mockRepository.save.mockReturnValue(
      TaskEither.right({
        id: 'order-123',
        status: 'draft',
        customerId: command.customerId,
        items: [{
          productId: 'item-1',
          quantity: 1,
          unitPrice: { amount: 1000, currency: 'JPY' }
        }],
        totalAmount: { amount: 1000, currency: 'JPY' },
        createdAt: new Date(),
        updatedAt: new Date()
      } as Order)
    );

    // Act
    const result = await createHandler(command)();

    // Assert
    expect(E.isRight(result)).toBe(true);
    expect(mockRepository.save).toHaveBeenCalledTimes(1);

    if (E.isRight(result)) {
      expect(result.right.customerId).toBe(command.customerId);
      expect(result.right.status).toBe('draft');
    }
  });

  it('バリデーションエラーのハンドリング', async () => {
    // Arrange
    const invalidCommand = {
      customerId: 'invalid-uuid', // 不正なUUID
      items: []
    } as CreateOrderCommand;

    // Act
    const result = await createHandler(invalidCommand)();

    // Assert
    expect(E.isLeft(result)).toBe(true);
    expect(mockRepository.save).not.toHaveBeenCalled();

    if (E.isLeft(result)) {
      expect(result.left.type).toBe('ValidationError');
    }
  });
});

段階的開発プロセス

Phase 1: ドメイン層の構築

# ステップ 1: 基本型定義
touch src/shared/types.ts
touch src/order/domain/types.ts

# ステップ 2: 型チェック実行
npm run type-check

# ステップ 3: 基本関数実装
touch src/order/domain/functions.ts

# ステップ 4: ドメインテスト作成
touch src/order/domain/__tests__/functions.test.ts

実装チェックリスト:

  • Zodスキーマ定義完了
  • ブランド型とエイリアス定義
  • 状態遷移マップ作成
  • 基本ドメイン関数実装
  • 単体テスト作成・実行

Phase 2: アプリケーション層の構築

# ステップ 5: サービス層実装
touch src/order/application/service.ts

# ステップ 6: リポジトリインターフェース定義
# ステップ 7: エラー型とハンドラー実装

# ステップ 8: 統合テスト作成
touch src/order/application/__tests__/service.test.ts

実装チェックリスト:

  • TaskEitherパイプライン構築
  • エラーハンドリング実装
  • コマンド/クエリ分離
  • モックを使った統合テスト

Phase 3: インフラ層とE2Eテスト

# ステップ 9: リポジトリ実装
touch src/order/infrastructure/repository.ts

# ステップ 10: E2Eテスト作成
touch src/__tests__/e2e/order-flow.test.ts

# ステップ 11: 実行例作成
touch src/example.ts

最終チェックリスト:

  • npm run test 全テスト通過
  • npm run type-check エラーなし
  • npm run example 正常実行
  • npm run build 成功

品質保証パターン

失敗時のエスケープルール:

  1. 3回連続テスト失敗 → 設計見直し、要件再確認
  2. 型エラーが5個以上 → アーキテクチャ再検討
  3. 関数が20行超過 → 関数分割、単一責任原則確認

コードレビューチェックポイント:

  • 純粋関数の原則遵守
  • 適切なエラーハンドリング
  • 型安全性の確保
  • テストカバレッジ 80% 以上
  • ドキュメントと実装の一致

次のフィーチャーへの展開

同じプロセスで以下の機能を順次実装:

  1. 在庫管理システム (Inventory Aggregate)
  2. 顧客管理システム (Customer Aggregate)
  3. 決済処理システム (Payment Service)
  4. 通知システム (Notification Events)

各機能で Phase 1-3 を繰り返し、段階的にシステムを拡張していきます。

トラブルシューティング

よくある問題と解決方法

型エラー関連:

// ❌ 問題: Type 'string' is not assignable to 'OrderId'
const orderId: OrderId = "some-id";

// ✅ 解決: ブランド型の正しい作成
const orderId: OrderId = createOrderId();
// または
const orderId = "some-id" as OrderId; // 型アサーション(注意して使用)

fp-ts関連:

// ❌ 問題: Cannot find namespace 'E'
const result = E.right(value);

// ✅ 解決: 正しいインポート
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

Zodバリデーション:

// ❌ 問題: 期待した型にならない
const order = OrderSchema.parse(data); // throwする

// ✅ 解決: safeParseを使用
const result = OrderSchema.safeParse(data);
if (result.success) {
  const order = result.data; // 型安全
} else {
  console.error(result.error);
}

状態遷移エラー:

// ❌ 問題: 「状態 'shipped' からアクション 'CONFIRM' は実行できません」
// ✅ 解決: 利用可能アクションの事前確認
const availableActions = OrderUtils.getAvailableActions(order);
if (availableActions.includes('CONFIRM')) {
  // 実行可能
}

まとめ

このガイドは、TypeScriptと関数型プログラミングを使用したDDD実装の包括的なリファレンスです。

主要な特徴:

  • 型安全性: Zodとブランド型による実行時・コンパイル時検証
  • 純粋性: 副作用のない純粋関数によるドメインロジック
  • 合成可能性: TaskEitherモナドによる関数合成
  • 保守性: 明確な責任分離とテスト可能な設計

実装効果:

  • バグの早期発見(コンパイル時)
  • 状態不整合の防止(型レベル)
  • テストの容易性(純粋関数)
  • 拡張性の向上(関数合成)

このアプローチにより、複雑なビジネスロジックを型安全で保守可能な形で実装できます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment