<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>DDD on 怠惰技術ブログ</title>
    <link>/tags/ddd/</link>
    <description>Recent content in DDD on 怠惰技術ブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Thu, 05 Mar 2026 23:00:00 +0900</lastBuildDate>
    <atom:link href="/tags/ddd/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その6</title>
      <link>/posts/2026-03-05-test-driven-design-readmeter-6/</link>
      <pubDate>Thu, 05 Mar 2026 23:00:00 +0900</pubDate>
      <guid>/posts/2026-03-05-test-driven-design-readmeter-6/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;Part5でUIまで実装が終わった。機能として動くものはできた。テストも15件、全パス。&lt;/p&gt;
&lt;p&gt;ここで一度立ち止まって、&lt;strong&gt;なぜこの設計になったのか&lt;/strong&gt;を振り返る。&lt;/p&gt;
&lt;p&gt;正直に言う。最初の要件はこうだった。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「クリーンアーキテクチャっぽく、テスタブルにしたい」&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;それだけだった。クリーンアーキテクチャを理解して設計したわけじゃない。
&lt;strong&gt;結果的にクリーンアーキテクチャを遂行したのは生成AIだ。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;そしてPart3以降、自分はほとんど手を動かさなくなった。
設計ドキュメントをAIに渡したら、実装サイクルがほぼ自動で回るようになったからだ。&lt;/p&gt;
&lt;p&gt;途中でこう思った。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「俺いる必要なくね？」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;この記事はその問いに向き合いながら、設計を改めて言語化したものだ。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;このシリーズ自体がtddだった&#34;&gt;このシリーズ自体がTDDだった&lt;/h2&gt;
&lt;p&gt;書き終えてから気づいたことがある。&lt;/p&gt;
&lt;p&gt;このシリーズのサイクルはこうだった。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;AIに実装させる
  ↓
動かす・読む・会話する（認識のズレを検出）
  ↓
ズレを言語化してAIにフィードバック
  ↓
納得したら記事にする
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ソフトウェアのTDDは「Red → Green → Refactor」だけど、
自分がやっていたのはこれだ。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Red&lt;/strong&gt; → 認識がズレていると感じる&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Green&lt;/strong&gt; → 会話して納得する&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor&lt;/strong&gt; → 記事として言語化する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;人間がテストケースになっていた。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TDDが「仕様をテストで表現する」なら、自分がやっていたのは「理解をフィードバックループで検証する」だ。構造は同じだと思う。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;誤っていた認識たち&#34;&gt;誤っていた認識たち&lt;/h2&gt;
&lt;p&gt;振り返ると、理解がズレていた箇所がいくつかあった。&lt;/p&gt;
&lt;h3 id=&#34;policyはvoと1-1になると思っていた&#34;&gt;「PolicyはVOと1-1になる」と思っていた&lt;/h3&gt;
&lt;p&gt;最初、&lt;code&gt;ReadingStatusPolicy&lt;/code&gt;が&lt;code&gt;ReadingStatus&lt;/code&gt;に対応しているのを見て、PolicyはVOと対になるものだと思っていた。&lt;/p&gt;
&lt;p&gt;違う。今回たまたま1-1になっているだけだ。&lt;/p&gt;
&lt;p&gt;Policyの本質は&lt;strong&gt;複数のEntityやVOをまたいだ条件判定&lt;/strong&gt;だ。たとえば「同じ本に同じユーザーが2回レビューできない」というルールは、Book・User・Review[]をまたぐ。これをPolicyに出す。&lt;/p&gt;
&lt;p&gt;VOに閉じるルールならVOに書けばいい。Entityをまたぐ判定が必要になったときにPolicyの出番だ。&lt;/p&gt;
&lt;h3 id=&#34;domainは外部を知らないという捉え方が逆だった&#34;&gt;「Domainは外部を知らない」という捉え方が逆だった&lt;/h3&gt;
&lt;p&gt;「DomainはDBを知らない」という言い方をよくするが、正確には逆だ。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部がDomainを知っている。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;向きの問題だ。Domainが何かを避けているのではなく、依存の矢印がすべてDomainに向かって刺さっている。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Route Handler
  ↓
Service
  ↓
Repository interface
  ↓
Domain（Entity / ValueObject / Policy）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;この向きがわかって初めて、Serviceの役割も見えた。&lt;/p&gt;
&lt;h3 id=&#34;serviceが何をするのかわかっていなかった&#34;&gt;Serviceが何をするのかわかっていなかった&lt;/h3&gt;
&lt;p&gt;依存の向きが腑に落ちるまで、Serviceが何者なのかずっと曖昧だった。&lt;/p&gt;
&lt;p&gt;答えはシンプルだった。&lt;strong&gt;フローだけ持つ接着剤。&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;startReading&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Book&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;findById&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 取得
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;NotFoundError&lt;/span&gt;(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;changeStatus&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ReadingStatus&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Reading&lt;/span&gt;);     &lt;span style=&#34;color:#75715e&#34;&gt;// Entityのルールに従う
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;);                  &lt;span style=&#34;color:#75715e&#34;&gt;// 保存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ServiceはDomainのルールを&lt;strong&gt;自分で判定しない&lt;/strong&gt;。
&lt;code&gt;canTransition&lt;/code&gt;を自分で呼ばない。&lt;code&gt;book.changeStatus()&lt;/code&gt;に委ねるだけだ。
判定はEntity・VO・Policyが持っている。Serviceはその結果を使ってフローを組む。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その4</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-4/</link>
      <pubDate>Thu, 05 Mar 2026 00:50:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-4/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part1&#34;&gt;Part1&lt;/a&gt; でValueObject・Policy・Entityを作り、&lt;a href=&#34;./part2&#34;&gt;Part2&lt;/a&gt; でServiceをDI＋Mockテストで固めた。
&lt;a href=&#34;./part3&#34;&gt;Part3&lt;/a&gt; でPrismaを繋ぎ、&lt;code&gt;GET /api/books&lt;/code&gt; と &lt;code&gt;POST /api/books&lt;/code&gt; を動かした。&lt;/p&gt;
&lt;p&gt;Part4では残りのエンドポイントを実装する。やることは3つ。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;カスタム例外クラスの導入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH /api/books/:id/start&lt;/code&gt; と &lt;code&gt;PATCH /api/books/:id/complete&lt;/code&gt; の実装&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /api/books/:id&lt;/code&gt; の実装&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;そして「&lt;strong&gt;エラー種別ごとにHTTPステータスを整理する&lt;/strong&gt;」という設計判断を掘り下げる。&lt;/p&gt;
&lt;p&gt;また、Part3でPrisma v7特有のセットアップをしたが、v6以前と何が変わったのかをここで整理しておく。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-prisma-v7で何が変わったか&#34;&gt;0. Prisma v7で何が変わったか&lt;/h2&gt;
&lt;p&gt;Part3でPrismaをセットアップしたとき、v6以前と比べていくつか「見慣れない書き方」が必要だった。
v7は破壊的変更が多く、ネット上のv6時代の記事を参考にすると詰まる箇所がある。
ここで整理しておく。&lt;/p&gt;
&lt;p&gt;参考: &lt;a href=&#34;https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7&#34;&gt;Upgrade to Prisma ORM 7 | Prisma Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;generator-の変更&#34;&gt;generator の変更&lt;/h3&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-prisma&#34; data-lang=&#34;prisma&#34;&gt;// ❌ v6以前
generator client {
  provider = &amp;#34;prisma-client-js&amp;#34;
}

// ✅ v7
generator client {
  provider = &amp;#34;prisma-client&amp;#34;
  output   = &amp;#34;../src/generated/prisma&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;v7では &lt;code&gt;prisma-client-js&lt;/code&gt; が廃止され &lt;code&gt;prisma-client&lt;/code&gt; に変わった。
Rustベースのエンジンを廃止しTypeScriptネイティブになったことに伴う変更だ。
また &lt;code&gt;output&lt;/code&gt; が必須になり、&lt;code&gt;node_modules&lt;/code&gt; への自動生成はなくなった。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その3</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-3/</link>
      <pubDate>Wed, 04 Mar 2026 17:50:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-3/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part1&#34;&gt;Part1&lt;/a&gt; でValueObject・Policy・Entityを作り、&lt;a href=&#34;./part2&#34;&gt;Part2&lt;/a&gt; でServiceをDI＋Mockテストで固めた。
累計13テスト、全パスの状態だ。&lt;/p&gt;
&lt;p&gt;Part3ではいよいよDBを繋ぐ。やることは3つ。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prismaセットアップ＋スキーマ定義&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PrismaBookRepository&lt;/code&gt; 実装（ドメインオブジェクトへの変換）&lt;/li&gt;
&lt;li&gt;Route HandlerでDIを組み立てる&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;そして最後に「&lt;strong&gt;テストを書かない層を意図的に決める&lt;/strong&gt;」という話をする。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-prismaセットアップ&#34;&gt;1. Prismaセットアップ&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install prisma @prisma/client
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npx prisma init --datasource-provider sqlite
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt; に Bookモデルを定義する。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-prisma&#34; data-lang=&#34;prisma&#34;&gt;generator client {
  provider = &amp;#34;prisma-client-js&amp;#34;
}

datasource db {
  provider = &amp;#34;sqlite&amp;#34;
  url      = env(&amp;#34;DATABASE_URL&amp;#34;)
}

model Book {
  id     String  @id
  title  String
  isbn   String
  status String
  rating Int?
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;User&lt;/code&gt; と &lt;code&gt;Review&lt;/code&gt; はまだDBに持たない。Book の CRUD が動けば Part3 のゴール。&lt;/p&gt;
&lt;h3 id=&#34;なぜ-status-を-string-で持つのか&#34;&gt;なぜ &lt;code&gt;status&lt;/code&gt; を String で持つのか&lt;/h3&gt;
&lt;p&gt;Prismaは SQLiteで enum をネイティブサポートしていない。
そのため &lt;code&gt;status String&lt;/code&gt; で持ち、取り出し時に &lt;code&gt;as ReadingStatus&lt;/code&gt; でキャストする。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
