1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js】リファクタリングを通して学ぶドメイン駆動設計 Part3: リポジトリインターフェースの定義

Posted at

はじめに

この記事は、以下の記事の続きです。

今回はリポジトリインターフェースの定義について説明します。

開発環境

開発環境は以下の通りです。

  • 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 という名前を名乗るクラスは、必ず savefindById などのメソッドを実装しなければならない」というルールを定めます。

ドメイン層は、このインターフェースを通じて**「どんな機能が必要か」**という要求だけを表明します。

ドメイン層でインターフェースを定義

まずドメイン層でインターフェースを定義します。

domain/repositories/IUserRepository.ts
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を変更しても、ドメイン層のコードに影響が出にくい
    • 責務の分離:ビジネスロジックがインフラの詳細から解放され、純粋になる

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?