はじめに
この記事は、以下の記事の続きです。
今回はリポジトリインターフェースの定義について説明します。
開発環境
開発環境は以下の通りです。
- Windows11
- Docker Engine 27.0.3
- Docker Compose 2
- PostgreSQL 18.1
- Node.js 24.11.0
- npm 11.6.2
- TypeScript 5.9.3
- Express 5.1.0
- Prisma 6.18.0
- Zod 4.1.12
リポジトリ(Repository)とは
リポジトリ(Repository)とは、「データベースのようなデータの永続化機構」と「ドメイン層(ビジネスロジック)」の間を取り持つ、仮想的なデータコレクション(データの保管場所)です。
データの永続化とは、データをデータベースなどに保存して、アプリケーションを終了してもデータが消えないようにすることです。
ドメイン層はリポジトリに対して「このIDのユーザーをください」と依頼するだけで、データの保存や取得の具体的な手順を知る必要がなくなります。エンティティ(例:User)の永続化(保存・取得・更新・削除)に関する具体的な技術(Prisma, SQL, NoSQLなど)を抽象化することで、ドメイン層から見ると、リポジトリはメモリ上にデータがあるかのように振る舞います。
ここでいう「インターフェース」とは
TypeScriptのインターフェース (interface) は、契約(Contract) を定義する型の一種です。この契約は、「IUserRepository という名前を名乗るクラスは、必ず save、findById などのメソッドを実装しなければならない」というルールを定めます。
ドメイン層は、このインターフェースを通じて**「どんな機能が必要か」**という要求だけを表明します。
ドメイン層でインターフェースを定義
まずドメイン層でインターフェースを定義します。
export interface IUserRepository {
save(user: User): Promise<User>;
findById(id: UserId): Promise<User | null>;
findAll(): Promise<User[]>;
update(user: User): Promise<User>;
delete(id: UserId): Promise<void>;
existsByEmail(email: Email): Promise<boolean>;
}
なぜインターフェースが必要なのか
元のコードでは、コントローラーから直接Prismaを呼び出していました。
// Before: Prismaに直接依存
app.post("/users", async (req, res) => {
const user = await prisma.user.create({ ... }); // Prismaに直接依存
});
これには以下の問題があります。
問題1. テストが困難
ユニットテストを書く際、実際のデータベースが必要になってしまいます。
// Before: 実際のデータベースが必要
test('ユーザー作成', async () => {
// PostgreSQLが起動していないとテストできない
const user = await prisma.user.create({ ... });
});
問題2. データベースの変更が困難
将来、PrismaからTypeORMなど別の技術に変更する場合、アプリケーション全体を修正する必要があります。
問題3. ビジネスロジックとインフラが混在
データベースの詳細がビジネスロジックに漏れ出してしまいます。
リポジトリインターフェースを使うと、これらの問題が解決します。
// After: インターフェースに依存
export class CreateUserUseCase {
constructor(private readonly userRepository: IUserRepository) {}
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
const user = User.create(email, name);
const savedUser = await this.userRepository.save(user); // インターフェース経由
return savedUser.toObject();
}
}
また、インターフェースを使うことのメリットとしては以下のような点があります。
メリット1. テストが容易
モックに差し替えられるため、データベース不要でテストできます。
// After: モックでテスト可能
test('ユーザー作成', async () => {
const mockRepository: IUserRepository = {
save: jest.fn().mockResolvedValue(mockUser),
// ...
};
const useCase = new CreateUserUseCase(mockRepository);
// データベースなしでテストできる
});
メリット2. データベースの変更が容易
Prismaから別のORMに変更する場合、リポジトリの実装部分だけを修正すればよくなります。
// 実装を差し替えるだけ
const userRepository = new PrismaUserRepository(prisma);
// ↓
const userRepository = new TypeORMUserRepository(dataSource);
// ユースケースのコードは変更不要
const useCase = new CreateUserUseCase(userRepository);
メリット3. ビジネスロジックが純粋になる
ユースケースはデータベースの詳細を知る必要がありません。ユースケースのコードは「ユーザーを保存する」というビジネス上の意図だけを表現し、裏側のSQLやORMのコードに意識を割かずに済みます。
まとめ
今回は、DDDにおけるリポジトリインターフェースの役割と必要性について説明しました。
- ポイント
- リポジトリは永続化の抽象化であり、ドメイン層とインフラ層の間を取り持つ
- インターフェースは、リポジトリが提供する機能の契約を定義する
- ドメイン層はインターフェース(抽象)に依存し、具体的な実装(Prismaなど)には依存しない(依存性逆転の原則)
- メリット
- テストの独立性:モックに差し替えられるため、データベース不要でテスト可能
- 技術変更への耐性:ORMやDBを変更しても、ドメイン層のコードに影響が出にくい
- 責務の分離:ビジネスロジックがインフラの詳細から解放され、純粋になる