Repository Pattern
プロンプトに「Repository」を含めることで、ClaudeCodeはデータアクセス層を抽象化し、テスト容易性と保守性を向上させる実装を生成できます。弊社では、データベース操作、キャッシュ管理、外部ストレージとの連携などで データアクセスロジックをビジネスロジックから分離したい場合にRepositoryパターンを使用します。
弊社の推奨ルール
- データアクセスの抽象化 - 具体的な実装から独立したインターフェース
- ビジネスロジックとの分離 - ドメインロジックにデータアクセス詳細を含めない
- テスト可能性の向上 - モックやスタブでの単体テストを容易にする
- 複数データソース対応 - データベース、API、ファイルシステムを統一インターフェースで扱う
- トランザクション管理 - データ整合性を保証する仕組みの提供
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にまたがるトランザクションを管理します。