リポジトリパターン (GoF)

データアクセスロジックをカプセル化し、ビジネスロジックからデータストアの詳細を隠蔽するパターン

設計パターンDDD

リポジトリパターンとは

リポジトリパターンは、データアクセスロジックをカプセル化し、ビジネスロジックからデータストアの詳細を隠蔽するパターンである。DDD の基本構成要素で、テスト容易性と DB の差し替え可能性を提供する。詳細は「リポジトリパターン実装」を参照。

なぜ必要か

// ❌ ビジネスロジックに DynamoDB の詳細が混在
async function getOrder(id: string) {
  const result = await db.send(new GetItemCommand({
    TableName: 'orders',
    Key: { PK: { S: `ORDER#${id}` }, SK: { S: 'METADATA' } },
  }));
  return unmarshall(result.Item!);
}

// ✅ リポジトリで隠蔽
async function getOrder(id: string) {
  return orderRepository.findById(id);
}

TypeScript での実装

interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
  findByUserId(userId: string): Promise<Order[]>;
}

class DynamoDBOrderRepository implements OrderRepository {
  constructor(private db: DynamoDBClient, private tableName: string) {}

  async findById(id: string): Promise<Order | null> {
    const result = await this.db.send(new GetItemCommand({
      TableName: this.tableName,
      Key: { PK: { S: `ORDER#${id}` }, SK: { S: 'META' } },
    }));
    return result.Item ? toOrder(unmarshall(result.Item)) : null;
  }

  async save(order: Order): Promise<void> {
    await this.db.send(new PutItemCommand({
      TableName: this.tableName,
      Item: marshall(toItem(order)),
    }));
  }

  async findByUserId(userId: string): Promise<Order[]> {
    const result = await this.db.send(new QueryCommand({
      TableName: this.tableName,
      IndexName: 'GSI1',
      KeyConditionExpression: 'GSI1PK = :pk',
      ExpressionAttributeValues: { ':pk': { S: `USER#${userId}` } },
    }));
    return (result.Items ?? []).map(i => toOrder(unmarshall(i)));
  }
}

テスト用のインメモリ実装

class InMemoryOrderRepository implements OrderRepository {
  private store = new Map<string, Order>();
  async findById(id: string) { return this.store.get(id) ?? null; }
  async save(order: Order) { this.store.set(order.id, order); }
  async findByUserId(userId: string) {
    return [...this.store.values()].filter(o => o.userId === userId);
  }
}

リポジトリのメリット

テスト時に InMemory 実装に差し替えるだけで DB なしにテストでき、DynamoDB から Aurora への DB 変更もリポジトリの実装を差し替えるだけで済む。ビジネスロジックが DB の詳細を知らない状態を保てるため、データアクセスの責務が明確に分離される。

全体像を把握するには関連書籍も有用。

関連用語