リポジトリパターン

何をする設計か

リポジトリパターンは、データの取得・保存を専用の窓口(リポジトリ)へ集める設計です。業務ロジックは「どこに保存されているか」を知りません。

// 業務ロジック側 — 保存先を知らない
async function publish(id: string, repo: ArticleRepository) {
  const article = await repo.findById(id)
  if (!article) throw new Error('記事がありません')
  await repo.save({ ...article, published: true })
}

ArticleRepository の中身が SQL でも、REST API でも、ブラウザの localStorage でも、この関数は変わりません。「何を保存するか」と「どうやって保存するか」を分けるのがこのパターンの中核です。

ORMとの違い

混同されやすいので整理します。

ORMリポジトリ
役割テーブルとオブジェクトの変換データアクセスの窓口
抽象化の対象SQLの書き方保存先そのもの
差し替えDBの種類を替えるDBを使わない実装にも替えられる

[[ORM]] を使っていても、業務ロジックの中で直接クエリを組み立てているなら、保存先の都合はロジックに染み出しています。リポジトリはその1つ上の層で、ORMの呼び出し自体を内側に隠します。

何が嬉しいのか

1. テストが速く安定する

リポジトリはインターフェースなので、テストではメモリ上の実装を渡せます([[依存性注入(DIコンテナ)]] と組み合わせる)。DBを起動せずに業務ロジックを検証できます。

const repo = new InMemoryArticleRepository([{ id: 'a', published: false }])
await publish('a', repo)
expect(repo.items[0].published).toBe(true)

2. 保存先の変更が1か所で済む

localStorage で始めたアプリをサーバー保存へ移すとき、変わるのはリポジトリの実装だけです。呼び出し側は触りません。

3. クエリに名前が付く

findPublishedByCategory(category) のように、業務的な意味を持つ問い合わせとして並びます。同じ条件のクエリがあちこちに散らばるのを防げます。

純粋関数として書く

リポジトリの実装のうち、データの変換や絞り込みは副作用を持たない関数として切り出せます。

  • 入出力を伴う部分 — 実際の読み書き。ここはテストで差し替える
  • 変換・絞り込み — 引数から結果を計算するだけ。そのまま単体テストできる

この分け方をすると、ロジックの大半がI/O抜きで検証できます。テストのために非同期処理やモックを用意する量が減ります。

やりすぎの兆候

  • メソッドが findByXxx で無限に増える — 条件の組み合わせを全部メソッドにすると破綻します。条件オブジェクトを受け取る形へ寄せます
  • リポジトリがそのままORMの薄いラッパになる — save findById を素通しするだけなら、抽象の意味がありません。業務的な問い合わせが表れて初めて価値が出ます
  • 返す型が保存先の形のまま — テーブルの列そのままを返すと、結局その形に業務ロジックが縛られます。業務の型へ変換して返します

判断は「保存先を差し替える理由があるか」「同じ問い合わせが複数箇所に出るか」の2点です。どちらも無いなら、直接アクセスで構いません。

関連技術とのつながり

  • [[ORM]] — テーブルとオブジェクトの変換。リポジトリはその呼び出しを内側に隠す
  • [[依存性注入(DIコンテナ)]] — 実装を差し替えるための前提
  • [[テストダブル(モック・スタブ)]] — メモリ実装を渡してDB抜きで検証する
  • [[デザインパターン]] — 責務を分離する定番パターンのひとつ
  • [[ドメイン駆動設計(DDD)]] — 集約の永続化を担う窓口として位置づけられる
Q: リポジトリパターンとORMの違いとして正しいのはどれ?
- [ ] どちらも同じもの
- [x] ORMはテーブルとオブジェクトの変換、リポジトリは保存先そのものを隠す窓口
- [ ] リポジトリはSQLを速くする
解説: ORMを使っていても業務ロジックが直接クエリを組み立てていれば、保存先の都合が染み出しています。

Q: リポジトリを導入するとテストが速くなる理由はどれ?
- [ ] クエリが最適化されるから
- [x] メモリ上の実装に差し替えて、DBを起動せずに業務ロジックを検証できるから
- [ ] テストが並列に実行されるから
解説: 依存性注入と組み合わせることで、外部依存を持たない形で検証できます。

Q: リポジトリが「やりすぎ」になっている兆候はどれ?
- [x] `save` や `findById` をORMへ素通しするだけで、業務的な問い合わせが無い
- [ ] 業務的な意味を持つメソッド名が並んでいる
- [ ] 実装が複数ある
解説: 薄いラッパは抽象の意味がありません。業務の言葉で問い合わせが表れて初めて価値が出ます。