テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その6

はじめに Part5でUIまで実装が終わった。機能として動くものはできた。テストも15件、全パス。 ここで一度立ち止まって、なぜこの設計になったのかを振り返る。 正直に言う。最初の要件はこうだった。 「クリーンアーキテクチャっぽく、テスタブルにしたい」 それだけだった。クリーンアーキテクチャを理解して設計したわけじゃない。 結果的にクリーンアーキテクチャを遂行したのは生成AIだ。 そしてPart3以降、自分はほとんど手を動かさなくなった。 設計ドキュメントをAIに渡したら、実装サイクルがほぼ自動で回るようになったからだ。 途中でこう思った。 「俺いる必要なくね?」 この記事はその問いに向き合いながら、設計を改めて言語化したものだ。 このシリーズ自体がTDDだった 書き終えてから気づいたことがある。 このシリーズのサイクルはこうだった。 AIに実装させる ↓ 動かす・読む・会話する(認識のズレを検出) ↓ ズレを言語化してAIにフィードバック ↓ 納得したら記事にする ソフトウェアのTDDは「Red → Green → Refactor」だけど、 自分がやっていたのはこれだ。 Red → 認識がズレていると感じる Green → 会話して納得する Refactor → 記事として言語化する 人間がテストケースになっていた。 TDDが「仕様をテストで表現する」なら、自分がやっていたのは「理解をフィードバックループで検証する」だ。構造は同じだと思う。 誤っていた認識たち 振り返ると、理解がズレていた箇所がいくつかあった。 「PolicyはVOと1-1になる」と思っていた 最初、ReadingStatusPolicyがReadingStatusに対応しているのを見て、PolicyはVOと対になるものだと思っていた。 違う。今回たまたま1-1になっているだけだ。 Policyの本質は複数のEntityやVOをまたいだ条件判定だ。たとえば「同じ本に同じユーザーが2回レビューできない」というルールは、Book・User・Review[]をまたぐ。これをPolicyに出す。 VOに閉じるルールならVOに書けばいい。Entityをまたぐ判定が必要になったときにPolicyの出番だ。 「Domainは外部を知らない」という捉え方が逆だった 「DomainはDBを知らない」という言い方をよくするが、正確には逆だ。 外部がDomainを知っている。 向きの問題だ。Domainが何かを避けているのではなく、依存の矢印がすべてDomainに向かって刺さっている。 Route Handler ↓ Service ↓ Repository interface ↓ Domain(Entity / ValueObject / Policy) この向きがわかって初めて、Serviceの役割も見えた。 Serviceが何をするのかわかっていなかった 依存の向きが腑に落ちるまで、Serviceが何者なのかずっと曖昧だった。 答えはシンプルだった。フローだけ持つ接着剤。 async startReading(id: string): Promise<Book> { const book = await this.repo.findById(id); // 取得 if (!book) throw new NotFoundError(...); book.changeStatus(ReadingStatus.Reading); // Entityのルールに従う await this.repo.save(book); // 保存 return book; } ServiceはDomainのルールを自分で判定しない。 canTransitionを自分で呼ばない。book.changeStatus()に委ねるだけだ。 判定はEntity・VO・Policyが持っている。Serviceはその結果を使ってフローを組む。 ...

March 5, 2026 · 2 min