Interactive

Repository Pattern

プロンプトに「Repository」を含めることで、ClaudeCodeはデータアクセス層を抽象化し、テスト容易性と保守性を向上させる実装を生成できます。弊社では、データベース操作、キャッシュ管理、外部ストレージとの連携などで データアクセスロジックをビジネスロジックから分離したい場合にRepositoryパターンを使用します。

弊社の推奨ルール

  1. データアクセスの抽象化 - 具体的な実装から独立したインターフェース
  2. ビジネスロジックとの分離 - ドメインロジックにデータアクセス詳細を含めない
  3. テスト可能性の向上 - モックやスタブでの単体テストを容易にする
  4. 複数データソース対応 - データベース、API、ファイルシステムを統一インターフェースで扱う
  5. トランザクション管理 - データ整合性を保証する仕組みの提供

ClaudeCodeでの利用

データベース固有Repositoryの作成

特定のデータベースに対応したリポジトリを実装したい場合

「UserRepositoryインターフェースを実装したMongoDBリポジトリを作って」

テスト用Repositoryの生成

単体テスト用のインメモリ実装を用意したい場合

「テスト用のインメモリリポジトリも一緒に作成して」

直接DBアクセスの抽象化

SQLを直接書いているコードをパターンに変更する場合

「このSQL直書きのコードをRepositoryパターンに変更して」

高機能Repositoryの実装

キャッシュや複雑な機能を含むリポジトリを作成する場合

「キャッシュ機能付きのUserRepositoryを実装」

直接的なDBアクセスではなくRepositoryを使う

SQLやORMの直接操作をビジネスロジックに含めると、テストが困難になり、データベース変更時の影響範囲が広がります。

// ❌ 避ける:ビジネスロジックにDBアクセスが混在
class UserService {
  async activateUser(userId: string): Promise<void> {
    const user = await this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
    if (user.status === 'banned') {
      throw new Error('Cannot activate banned user');
    }

データベース更新とその他の処理が混在しています。

    await this.db.query('UPDATE users SET status = ? WHERE id = ?', ['active', userId]);
    // ログ出力など多数の処理...
  }
}

Repositoryパターンでデータアクセスを抽象化し、保守性を向上させます。

// ✅ 推奨:抽象インターフェースを定義
interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
  findActiveUsers(limit: number): Promise<readonly User[]>;
}

interface ActivityLogRepository {
  logUserAction(userId: string, action: string): Promise<void>;
}

データベース固有の実装を別クラスに分離します。

// データベース固有の実装
class DatabaseUserRepository implements UserRepository {
  constructor(private db: Database) {}
  
  async findById(id: string): Promise<User | null> {
    const row = await this.db.queryOne(
      'SELECT id, name, email, status FROM users WHERE id = ?', [id]
    );
    return row ? this.mapToUser(row) : null;
  }
  
  async save(user: User): Promise<void> {
    await this.db.query(
      'UPDATE users SET status = ?, activated_at = ? WHERE id = ?',
      [user.status, user.activatedAt, user.id]
    );
  }
}

ビジネスロジックはRepositoryの抽象インターフェースに依存します。

// ビジネスロジックはRepositoryに依存
class UserService {
  constructor(
    private userRepository: UserRepository,
    private activityLogRepository: ActivityLogRepository
  ) {}
  
  async activateUser(userId: string): Promise<void> {
    const user = await this.userRepository.findById(userId);
    if (!user || user.status === 'banned') {
      throw new Error('Cannot activate user');
    }

ユーザーの状態更新と履歴記録を行います。

    const activatedUser = { ...user, status: 'active', activatedAt: new Date() };
    await this.userRepository.save(activatedUser);
    await this.activityLogRepository.logUserAction(userId, 'user_activated');
  }
}

テスト用のインメモリ実装も簡単に作成できます。

// テスト用のインメモリ実装
class InMemoryUserRepository implements UserRepository {
  private users: Map<string, User> = new Map();
  
  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

インメモリストレージでの保存処理です。

  async save(user: User): Promise<void> {
    this.users.set(user.id, user);
  }
}

このパターンにより、ビジネスロジックがデータアクセス詳細から独立し、テストが容易になります。また、データベースの種類を変更する際も、Repository実装のみを変更すれば済みます。

よくある問題

Repositoryが肥大化する

単一のRepositoryに多くのメソッドが集中してしまう問題です。責務ごとにRepositoryを分割することで解決できます。

// ❌ 肥大化したRepository
interface MegaUserRepository {
  // 基本CRUD
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;

基本的なCRUD操作に加えて、検索系のメソッドが大量に追加されてしまいます。

  // 検索系メソッド(20個以上)
  findByEmail(email: string): Promise<User | null>;
  findByName(name: string): Promise<User[]>;
  findActiveUsers(): Promise<User[]>;
  findInactiveUsers(): Promise<User[]>;
  findBannedUsers(): Promise<User[]>;
  findUsersByRole(role: string): Promise<User[]>;
  findUsersByCreatedDate(from: Date, to: Date): Promise<User[]>;
  // ... さらに多数のメソッドが続く
}

このような肥大化したRepositoryは、責務ごとに分割することで管理しやすくなります。

// ✅ 責務ごとに分割 - 基本的なCRUD操作
interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

検索に特化したRepositoryを別途定義します。

// 検索専用のRepository
interface UserSearchRepository {
  findByEmail(email: string): Promise<User | null>;
  findByName(name: string): Promise<User[]>;
  searchUsers(query: UserSearchQuery): Promise<User[]>;
}

統計情報の取得に特化したRepositoryも分離します。

// 統計情報専用のRepository
interface UserStatsRepository {
  countActiveUsers(): Promise<number>;
  getUserStats(userId: string): Promise<UserStats>;
  getRegistrationTrends(period: Period): Promise<TrendData>;
}

このように責務ごとに分割することで、各Repositoryの役割が明確になり、テストも書きやすくなります。

Q: いつRepositoryパターンを使うべき?

A: 弊社ではデータアクセスが複数箇所に散らばる場合、またはテストでモックが必要な場合に使用します。

Q: ORMと組み合わせる場合の注意点は?

A: ORMのエンティティをそのまま返さず、ドメインモデルに変換してから返すことを推奨します。

Q: トランザクション管理はどうする?

A: Unit of Workパターンと組み合わせて、複数Repositoryにまたがるトランザクションを管理します。