リポジトリパターン
何をする設計か
リポジトリパターンは、データの取得・保存を専用の窓口(リポジトリ)へ集める設計です。業務ロジックは「どこに保存されているか」を知りません。
// 業務ロジック側 — 保存先を知らない
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の薄いラッパになる —
savefindByIdを素通しするだけなら、抽象の意味がありません。業務的な問い合わせが表れて初めて価値が出ます - 返す型が保存先の形のまま — テーブルの列そのままを返すと、結局その形に業務ロジックが縛られます。業務の型へ変換して返します
判断は「保存先を差し替える理由があるか」「同じ問い合わせが複数箇所に出るか」の2点です。どちらも無いなら、直接アクセスで構いません。
関連技術とのつながり
- [[ORM]] — テーブルとオブジェクトの変換。リポジトリはその呼び出しを内側に隠す
- [[依存性注入(DIコンテナ)]] — 実装を差し替えるための前提
- [[テストダブル(モック・スタブ)]] — メモリ実装を渡してDB抜きで検証する
- [[デザインパターン]] — 責務を分離する定番パターンのひとつ
- [[ドメイン駆動設計(DDD)]] — 集約の永続化を担う窓口として位置づけられる
Q: リポジトリパターンとORMの違いとして正しいのはどれ?
- [ ] どちらも同じもの
- [x] ORMはテーブルとオブジェクトの変換、リポジトリは保存先そのものを隠す窓口
- [ ] リポジトリはSQLを速くする
解説: ORMを使っていても業務ロジックが直接クエリを組み立てていれば、保存先の都合が染み出しています。
Q: リポジトリを導入するとテストが速くなる理由はどれ?
- [ ] クエリが最適化されるから
- [x] メモリ上の実装に差し替えて、DBを起動せずに業務ロジックを検証できるから
- [ ] テストが並列に実行されるから
解説: 依存性注入と組み合わせることで、外部依存を持たない形で検証できます。
Q: リポジトリが「やりすぎ」になっている兆候はどれ?
- [x] `save` や `findById` をORMへ素通しするだけで、業務的な問い合わせが無い
- [ ] 業務的な意味を持つメソッド名が並んでいる
- [ ] 実装が複数ある
解説: 薄いラッパは抽象の意味がありません。業務の言葉で問い合わせが表れて初めて価値が出ます。