この記事はシリーズ「動いているリポジトリを読む — Next.js 16 で学ぶクリーンアーキテクチャと TDD」の第 6 回(全 19 回)です。
実際に動いている公開リポジトリ kai-kou/gem-hunter(MIT)を1本まるごと読み解く連載です。架空のサンプルではなく実ファイルを引用し、掲載しているコマンド結果はすべて実際に動かして採取しています。
全体を通しで読みたい方へ: 同じ内容を 1 冊にまとめた Zenn Book を無料で公開しています。
シリーズ全体の目次
- 第 1 回 動いているリポジトリを読むという学び方
- 第 2 回 clone からテストが緑になるまで
- 第 3 回 要件IDとADRで、何を解くアプリなのかを地図にする
- 第 4 回 App Router の地図。フォルダがそのまま URL になる
- 第 5 回 'use client' が6ファイルしかないアプリの境界の引き方
- 第 6 回 「クリーンアーキテクチャだから」を理由にしない層の分け方(この記事)
- 第 7 回 ブランド型で「検証済みの値」を型にする
- 第 8 回 DIコンテナを使わない依存性逆転
- 第 9 回 外部APIの語彙を持ち込ませない翻訳層の作り方
- 第 10 回 依存規則を440行のPythonで機械検査する
- 第 11 回 URLのクエリが画面に出るまでを5層ぶん追跡する
- 第 12 回 ライブラリなしのi18nと、画面の変化を目で見ていない人へ伝える実装
- 第 13 回 どの層を何でテストするかを表で決めておく(公開予定)
- 第 14 回 失敗するテストを先に書いて Red→Green を1周する(公開予定)
- 第 15 回 「実ネットワークに出ない」を設定1行で保証する(公開予定)
- 第 16 回 外部APIに依存しないE2Eの組み方(公開予定)
- 第 17 回 赤くなったテストをどう判定するか、axeの限界とflakyの正体(公開予定)
- 第 18 回 生IPを残さないレート制限と、第三者HTMLを安全に表示する(公開予定)
- 第 19 回 CIは道具ではなくゲートの集合、そして自分のプロジェクトへの持ち帰り方(公開予定)
TL;DR
対象読者は、レイヤードアーキテクチャやクリーンアーキテクチャを「聞いたことはあるが、自分のプロジェクトで線をどこに引くべきか分からない」エンジニアです。
- gem-hunter の設計ドキュメントは「クリーンアーキテクチャだから、は理由にならない」と明記しています
- 層を分ける理由を 3 つだけ に限定し、それ以外の目的で層を増やすことを禁じています
- 採用しなかった DDD の要素(集約・リポジトリパターン・CQRS)も 明示的に書いてあります
「なんとなく分ける」と何が起きるか
層を分ける設計は、目的を見失うと簡単に形骸化します。よくある終着点はこうです。
- ファイルを開くたびに 3 つのディレクトリを行き来する
- 1 つのフィールドを足すのに 5 ファイル触る
- 「この処理はどの層?」の議論が毎回発生する
- 結局みんな面倒になって、どこかの層に何でも書き始める
そして誰も「なぜ分けているのか」を答えられなくなります。
gem-hunter の設計ドキュメントは、この状態を避けるために冒頭でこう宣言しています。
層を分ける目的は次の 3 つだけである。
それ以外の理由(「クリーンアーキテクチャだから」「一般にそうするから」)で層・DTO・変換を増やさない。
分ける理由は 3 つだけ
| ID | 守りたいこと | 具体的に何が起きうるか |
|---|---|---|
| W-1 | データ源の差し替え | いまは GitHub API だけだが、被依存数のデータ源が増える予定がある |
| W-2 | 事業者の差し替え | Cloudflare Workers 固有の機能に全体が依存すると移せなくなる |
| W-3 | 高速なテスト | 外部 API を叩かずに業務ロジックを検証したい |
そして運用ルールがあります。
新しい層・DTO・変換を足すときは、
W-1〜W-3のどれを守るためかを 1 行で言えること を条件にする。言えないなら足さない。
この 1 行が効いています。「一般的にはこう書くから」では層を増やせません。
依存の向き
層の中身より先に、依存の向き を押さえるのが近道です。
矢印は「A が B を import してよい」という向きです。ポイントは 2 つ。
-
domain/は何も import しない。React も Next.js も zod も入りません。純粋な TypeScript だけです -
infrastructure/はdomain/に「実装を合わせる」向き。GitHub API を叩くクラスが、domain が決めたインターフェースに従います
2 番目が「依存性逆転」と呼ばれるものです。普通に書くと「業務ロジック → GitHub API クライアント」と依存しますが、間にインターフェースを挟むことで 両方が真ん中のインターフェースを向く 形にできます。
import してよい/だめの表
gem-hunter では、これが表として明文化されています。
| 層 | import してよい | してはいけない |
|---|---|---|
domain/ |
domain/ と shared/ のみ |
それ以外すべて(外部パッケージも) |
usecases/ |
domain/ shared/
|
infrastructure/ ui/ app/ next react
|
infrastructure/ |
domain/ shared/
|
usecases/ ui/ app/
|
ui/ |
domain/ shared/
|
usecases/ infrastructure/ app/
|
composition/ |
すべて | — |
app/ |
composition/ ui/ domain/ shared/
|
infrastructure/(直接は禁止) |
shared/ |
外部パッケージのみ | 他の層すべて |
読み方のコツは「composition/ だけが全部を知っている」ことです。他の層は互いを知らず、組み立てだけが 1 箇所に集まっています。
そして domain/ だけ「禁止リスト」ではなく「許可リスト」になっています。禁止を並べる方式だと、新しく入れた外部パッケージを弾けないからです。
この表は文章として書いてあるだけでなく、Python スクリプトで機械的に検査されています。第 10 回で、実際にわざと破って落としてみます。
破ったときに何が起きるかまで書いてある
規則には ID が振られ、「破ったときに何が起きるか」の列があります。
| ID | 規則 | 破ると何が起きるか |
|---|---|---|
| ARCH-1 | ドメインは何にも依存しない | フレームワーク更新でドメインが壊れる |
| ARCH-2 | ユースケースはポートを引数で受け取る | テストで実 API を叩くしかなくなる |
| ARCH-3 | 画面から infrastructure を直接 import しない | 差し替え不能になる |
| ARCH-4 | 事業者固有の機能に触れてよいのは 1 ディレクトリだけ | 移行時に全ファイル調査が必要になる |
| ARCH-5 | 秘密情報を読んでよいのは 1 ディレクトリだけ | 漏洩経路が増える |
「守るべき」ではなく「破ると何が起きるか」で書いてあるので、規則を守る動機が規則自体に含まれています。
どこに書くか迷ったときの判定
新しい処理をどの層に置くか、5 つの質問で決められるようになっています。
③と④の見分け方が、実務ではいちばん迷うところです。設計ドキュメントの説明はこうです。
GitHub が無くても意味が通る規則なら domain。「検索キーワードは 1 文字以上 256 文字以下」はサービスに関係なく成り立つ規則なので domain。
画面の操作の段取りなら usecases。「検索して、結果が 0 件ならその旨を返す」は操作の手順なので usecases。
採らなかったものも書いてある
面白いのは、採用しなかった設計要素が名指しで書いてある ことです。
集約ルート・リポジトリパターン(永続化)・ドメインイベント・CQRS は、DB を持たない MVP では過剰設計 なので採らない。
これは「知らなかったから使っていない」と「知ったうえで使わないと決めた」を区別するための記述です。後から参加した人が「なんで集約ルートが無いの?」と思ったとき、答えがここにあります。
設計における「やらないことの明文化」は、やることの明文化と同じくらい重要です。 書いていないと、善意の誰かがいつか足してしまいます。
機械で守れないものは、レビュー観点として切り出す
すべての規則が機械検査できるわけではありません。gem-hunter では、機械検査できない 2 つを別立てにしています。
| ID | 規則 | なぜ機械で見られないか |
|---|---|---|
| ARCH-R1 | 外部レスポンスは検証してから domain へ渡す | 「検証したか」は静的解析では判断が難しい |
| ARCH-R2 | ユースケースの引数は値オブジェクトにする | 型は合っていても意味が正しいかは別問題 |
「機械で守れるもの」と「人が見るしかないもの」を分けて、後者はレビュー観点として明示する。 全部を自動化しようとして中途半端な検査を作るより、割り切りとして誠実です。
まとめ
- 層を分ける理由を 3 つに限定 し、それ以外の理由で層を増やさない
- 依存の向きは内向き一方通行。
domain/は何にも依存しない - 規則には「破ったときに何が起きるか」を併記する
- 採らなかった設計要素も明示 する(知らないのか、決めたのかを区別する)
- 機械で守れないものはレビュー観点として切り出す
シリーズの前後の記事
- ⬅️ 前の記事: 第 5 回 'use client' が6ファイルしかないアプリの境界の引き方
- ➡️ 次の記事: 第 7 回 ブランド型で「検証済みの値」を型にする
全 19 回をまとめて読みたい方は、同じ内容の Zenn Book(無料)へどうぞ。