TypeScriptで学ぶプログラミングの世界 Part 7 [アーキテクチャって何だ！？ただ動けばいいコードからの脱出をしよう！(クリーンアーキテクチャ)]

ソフトウェア開発において、プロジェクトの初期段階で立てた仕様が未来永劫変わらない、ということはまずあり得ないですよね？
ビジネスの成長や技術の進化に伴い、「データベースを別のものに差し替えたい」「既存のロジックを再利用して、Web APIだけでなくCLIツールも提供したい」といった要求は、ごく自然に発生するでしょう...

しかし、これらの「当たり前」のはずの要求が、なぜしばしば大規模で困難なコード修正を伴うのだろうか？

その原因の多くは、アプリケーションの「構造」、すなわちアーキテクチャに潜んでいると思います...
ビジネスの核心的なルールを担うコードが、データベース操作や特定フレームワークの作法といった技術的な詳細と密接に絡み合っている場合、一部の変更がシステム全体に波及してしまうことはよくある経験は多いのではないでしょうか？

今回はこうした課題を克服し、変更に強く、保守性の高いシステムを構築するため、将来の変更に柔軟に対応できる「クリーンアーキテクチャ」の構築方法を説明していきます．

# シリーズTypeScriptで学ぶプログラミングの世界
**Part1** 手続型からオブジェクト指向へ（今回はオブジェクト指向の話が関わるのでこちらを読んでから！)

https://qiita.com/JavaLangRuntimeException/items/cadf49bb419076819963

**Part2** ORMってなんなんだ？SQLとオブジェクト指向のミスマッチを感じませんか？

https://qiita.com/JavaLangRuntimeException/items/89fb282cd00c7daf40e6

**Part3** プログラミングパラダイムの進化と革命：機械語からマルチパラダイムへ...新しいプログラミング言語に出会ってみよう！

https://qiita.com/JavaLangRuntimeException/items/fa31d112f2d473748cfe

**Part4** アクセス修飾子とは? public? private? protected?

https://qiita.com/JavaLangRuntimeException/items/b9d114d9ffc672e393d3

**Part5** 総称型(ジェネリクス・型パラメータ)とは？

https://qiita.com/JavaLangRuntimeException/items/24c3e80a69401bf9adec

**Part6** staticってなんなんだ？

https://qiita.com/JavaLangRuntimeException/items/3ec567955e3b0be8b58d

# 他のシリーズ記事
**TypeScriptを知らない方は以下の記事を参照してください**

https://qiita.com/JavaLangRuntimeException/items/5894391c08e0d8e28389

> この記事はチートシートとしてシリーズ化しています．これは様々な言語，フレームワーク，ライブラリなど開発技術の使用方法，基本事項，応用事例を網羅し，手引書として記載したシリーズです．git/gh，lazygit，docker，vim，typescript，プルリクエスト/マークダウン，ステータスコード，ファイル操作，OpenAI AssistantsAPI，Ruby/Ruby on Rails のチートシートがあります．上の記事に遷移した後，各種チートシートのリンクがあります.

**情報処理技術者試験合格への道 [IP・SG・FE・AP]**
情報処理技術者試験に出題されるコンピュータサイエンス用語の紹介や単語集

https://qiita.com/JavaLangRuntimeException/items/991be402099542ccb936

**IAM AWS User クラウドサービスをフル活用しよう！**
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです．

https://qiita.com/JavaLangRuntimeException/items/371a334f5a6e07035db5

**AWS UserのGCP浮気日記**
GCPの様子をAWSと比較して考えてみるシリーズ

https://qiita.com/JavaLangRuntimeException/items/527d99e774165a763180

**Project Gopher: Unlocking Go’s Secrets**
Go言語や標準ライブラリの深掘り調査レポート

https://qiita.com/JavaLangRuntimeException/items/dc45b412d3fbd2ccb9e8

**Gopher’s Journey: Exploring TCP Protocol**
Goを用いてTCPプロトコルを探求 & 作成するシリーズです．

https://qiita.com/JavaLangRuntimeException/items/38091220106d86651d2b


## 1. はじめに：使用する技術スタック
- **TypeScript:** 静的型付けにより、コードの堅牢性と開発者体験が高い言語
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/05fa3eba-dfaf-4e58-9f8a-20905af59c52.png)

- **Bun:** 高速なJavaScriptランタイム、バンドラ、テストランナー、パッケージマネージャを兼ね備えたオールインワンツールキットである。
![スクリーンショット 2025年5月23日.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/5b38e270-8a52-4ef3-a5a9-df1deb2b850a.png)

-  **Hono.js**:
高速かつ軽量なWebフレームワークである。Cloudflare Workersのようなエッジ環境からNode.js、そしてBunまで、様々なJavaScriptランタイムで動作する。シンプルで使いやすいAPIを提供する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/136c73cf-1e72-4c34-9c0b-ec419a915bf5.png)

-  **Zod**:
TypeScriptファーストのスキーマ宣言および検証ライブラリである。静的なTypeScriptの型定義から、実行時のバリデーターを推論できるため、データの整合性を安全かつ容易に保つことができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/48a3ea03-11b6-4bbb-bbca-7645172e12c1.png)

- **MySQL & Redis:**
リレーショナルデータベースとインメモリキーバリューストアの代表格であり、永続化とキャッシングの役割を担う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/179f6d7b-9d14-4b32-892e-423adf8c47e8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/91bc9d06-c06a-4d04-9d55-6c7b28030492.png)

- **Prisma**:
次世代のNode.jsおよびTypeScript向けORM(Object-Relational Mapper)である。`schema.prisma`というスキーマファイルでデータモデルを定義すると、型安全なデータベースアクセスクライアントが自動生成される。これにより、手書きのSQLを減らし、開発効率と安全性を向上させる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/349f6919-bb6d-4816-9294-35acbc30d70e.png)

- **uuid**:
- 汎用一意識別子(UUID)を生成するためのライブラリである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/219efff4-68c1-47ab-86bc-e88c390e1bc5.png)

- **Docker & Docker Compose:** コンテナ技術を用いて、開発環境と本番環境の差異をなくし、ポータブルなアプリケーション実行環境を構築する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/0823e05b-dc4e-409e-ba16-360b070bf436.png)

- **ESLint:** コード品質を維持するためのリンターであり，flatconfig(`eslint.config.js`) 形式で設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/ff052784-41aa-4a23-8176-f5ee0a08f887.png)

Bun, Hono, Prisma ORMを使い、クリーンアーキテクチャでMySQLおよびRedisと通信するREST APIをDockerで構築する方法を、詳細な説明とコード例を交えて包括的に解説する。

### 2. 環境構築

まず、全てのアーキテクチャで共通となる開発環境を構築する。

:::note info
docker, bunは以下のように導入する

**Dockerの導入**

macOS:
```bash
# Homebrewを使用してDockerをインストール
brew install --cask docker
```

Windows:

公式サイトからDocker Desktopをダウンロードしてインストール

https://www.docker.com/products/docker-desktop/


**Bunの導入**

macOS/Linux:
```bash
# curlを使用してインストール

# bashの場合
curl -fsSL https://bun.sh/install | bash
exec /bin/bash

# zshの場合
curl -fsSL https://bun.sh/install | zsh
exec /bin/zsh

```

Windows:
```bash
# PowerShellで実行
powershell -c "irm bun.sh/install.ps1 | iex"

# または、npmを使用してインストール
npm install -g bun
```

インストール後、以下のコマンドでバージョンを確認できます：
```bash
docker --version
bun --version
```

:::

#### 2.1. ディレクトリとファイルの作成
本記事では、クリーンアーキテクチャを紹介しますが，ここからは一旦共通で使うディレクトリをセットアップしていきます．

```bash
# プロジェクトのルートディレクトリを作成
mkdir hono-backend-architecture-guide
cd hono-backend-architecture-guide

# Dockerと設定ファイルを先に作成
touch Dockerfile
touch docker-compose.yml
touch tsconfig.json
touch eslint.config.js
```

#### 2.2. BunおよびPrismaプロジェクトの初期化

```bash
# Bunプロジェクトを初期化し、package.jsonを生成
bun init -y

# Prismaのセットアップコマンド。prisma/schema.prisma と .env ファイルが生成される
bunx prisma init --datasource-provider mysql
```

:::note info
**bunxとは？**
bunxはBunのパッケージマネージャであるBunxを使用して、コマンドラインからパッケージを実行するためのツール
:::

#### 2.3. 依存ライブラリのインストール
bun addを用いることで、package.jsonに依存ライブラリを追加できる

```bash
# アプリケーションの依存ライブラリをインストール
# Prisma ClientはPrismaが生成するORMクライアント
bun add hono @hono/node-server @hono/zod-validator zod redis uuid @prisma/client

# 開発用の依存ライブラリをインストール
# Prisma CLI、TypeScriptと各種型定義、ESLint
bun add -d prisma typescript @types/node @types/uuid eslint @eslint/js typescript-eslint
```

#### 2.4. 設定ファイルの記述

**.env**
`prisma init`で生成された`.env`ファイルを編集し、Dockerコンテナ環境のデータベースURLを設定する。(後ほど設定します)
``` .env
DATABASE_URL="mysql://root:rootpassword@db:3306/testdb"
```

**paclage.json**
バージョンは任意のものをご利用ください
```package.json
{
  "name": "hono-backend-architecture-guide",
  "private": true,
  "scripts": {
    "start": "bun run src/index.ts",
    "dev": "bun run src/index.ts"
  },
  "devDependencies": {
    "@eslint/js": "^9.27.0",
    "@types/bun": "latest",
    "@types/node": "^22.15.21",
    "@types/uuid": "^10.0.0",
    "eslint": "^9.27.0",
    "prisma": "^6.8.2",
    "typescript-eslint": "^8.32.1"
  },
  "peerDependencies": {
    "typescript": "^5.8.3"
  },
  "dependencies": {
    "@hono/node-server": "^1.14.2",
    "@hono/zod-validator": "^0.5.0",
    "@prisma/client": "^6.8.2",
    "hono": "^4.7.10",
    "redis": "^5.1.0",
    "uuid": "^11.1.0",
    "zod": "^3.25.28"
  }
}

```

**prisma/schema.prisma**
データベースのテーブル構造を定義するファイル。今回はuserテーブルを作成する。userテーブルにはid, email, nameの3つのカラムを作成する。

```prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "linux-arm64-openssl-3.0.x"] // Dockerコンテナ（Linux環境）で動作させるためLinux ARM64環境でOpenSSL 1.1.x用の現在のマシン（開発環境）用のバイナリを生成
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id    String @id @default(uuid())
  email String @unique
  name  String

  @@map("users") // データベース上のテーブル名を'users'に指定
}

```

**tsconfig.json**
TypeScriptコンパイラの設定ファイル。

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "types": ["bun-types"]
  },
  "include": ["src"]
}
```

**eslint.config.js**
ESLintの設定ファイル。

:::note info
**Eslintとは？**
EslintはJavaScriptやTypeScriptのコードを静的解析し、コードの品質を維持するためのツールです。
例えば、以下のようなコードはEslintでエラーとなります。
```ts
// 未使用の変数（no-unused-vars）
const unusedVariable = 'this is not used';

// セミコロンの不一致（semi）
const message = 'Hello World'

// 未定義の変数の使用（no-undef）
console.log(undefinedVariable);

// 等価演算子の使用（eqeqeq）
if (value == null) {
  // === を使うべき
}
```

これらのエラーを事前に検出することで、バグの発生を防ぎ、コードの品質を向上させることができる

:::

ここでは、eslint.config.jsを作成します。


```eslint.config.js
import globals from "globals";
import tseslint from "typescript-eslint";

export default [
  { files: ["**/*.{js,mjs,cjs,ts}"] },
  { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
  ...tseslint.configs.recommended,
];
```

この設定では以下のことを行っています：

1. **対象ファイルの指定** (`files`):
   - `**/*.{js,mjs,cjs,ts}`: プロジェクト内のすべてのJavaScript（.js, .mjs, .cjs）とTypeScript（.ts）ファイルを対象とする

2. **グローバル変数の設定** (`languageOptions.globals`):
   - `globals.browser`: ブラウザ環境のグローバル変数（`window`, `document`, `console`など）を認識
   - `globals.node`: Node.js環境のグローバル変数（`process`, `Buffer`, `__dirname`など）を認識
   - これにより、これらの変数を使用してもESLintが「未定義の変数」エラーを出さなくなる

3. **TypeScript推奨ルールの適用** (`tseslint.configs.recommended`):
   - TypeScript用の推奨ルールセットを適用
   - 型安全性を高めるルール、TypeScript特有の構文チェックなどが含まれる
   - 例：`@typescript-eslint/no-unused-vars`(未使用の変数を検出), `@typescript-eslint/no-explicit-any`(any型を検出)など

**Dockerfile**
アプリケーションをコンテナ化するための設計図。


```dockerfile
FROM oven/bun:latest
WORKDIR /app

# OpenSSLをインストール（Prismaの警告を解決）
RUN apt-get update -y && apt-get install -y openssl

# 最初に依存関係ファイルのみをコピー (bun.lockb を使用)
COPY package.json bun.lock ./

# Prismaのスキーマファイルも先にコピー
COPY prisma ./prisma/

# 依存関係をインストール (frozen-lockfile を使用)
RUN bun install --frozen-lockfile

# アプリケーションのソースコードをコピー
COPY . .

# ★ Prisma Clientを生成する
RUN bunx prisma generate

# アプリケーションがリッスンするポートを公開
EXPOSE 3000

# アプリケーションを起動（起動前にPrisma Clientを再生成）
CMD ["sh", "-c", "bunx prisma generate && bun run src/index.ts"]

```

**docker-compose.yml**
複数コンテナを管理するファイル。

```yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    environment:
      # .envファイルからDATABASE_URLを読み込む
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    tty: true
    stdin_open: true

  db:
    image: mysql:8.0
    # .envファイルから直接環境変数を読み込むことも可能
    env_file:
      - .env
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      # docker-compose.yml内の環境変数は.envファイルの値より優先される場合があるため注意
      MYSQL_DATABASE: testdb
      MYSQL_USER: root
      MYSQL_PASSWORD: rootpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7.0
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mysql_data:
  redis_data:
```

これでコンテナのセットアップは完了です．次はアプリケーションのコードを書いていきます！

---

### 番外編：アーキテクチャがないとどうなる？
アーキテクチャがないと、コードが肥大化してしまい、保守性が低下します。
例えば以下のように1つのindex.tsファイルに全てのコードを書いた場合...

```ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { PrismaClient } from '@prisma/client';
import { createClient } from 'redis';
import { v4 as uuidv4 } from 'uuid';

const app = new Hono();
const prisma = new PrismaClient();
const redis = createClient({ url: 'redis://localhost:6379' });

// ユーザー作成API
app.post('/users', async (c) => {
  try {
    const { name, email } = await c.req.json();

    // バリデーション（ここに直接書く）
    if (!name || !email) {
      return c.json({ error: 'Name and email are required' }, 400);
    }
    if (!email.includes('@')) {
      return c.json({ error: 'Invalid email format' }, 400);
    }

    // データベース操作（ここに直接書く）
    const existingUser = await prisma.user.findUnique({
      where: { email }
    });
    if (existingUser) {
      return c.json({ error: 'Email already exists' }, 409);
    }

    // ユーザー作成
    const newUser = await prisma.user.create({
      data: {
        id: uuidv4(),
        name,
        email
      }
    });

    // キャッシュに保存（ここに直接書く）
    await redis.set(`user:${newUser.id}`, JSON.stringify(newUser));

    // ログ出力（ここに直接書く）
    console.log(`User created: ${newUser.id}`);

    return c.json(newUser, 201);
  } catch (error) {
    console.error('Error creating user:', error);
    return c.json({ error: 'Internal server error' }, 500);
  }
});

// ユーザー取得API
app.get('/users/:id', async (c) => {
  try {
    const id = c.req.param('id');

    // キャッシュから取得を試す（ここに直接書く）
    const cached = await redis.get(`user:${id}`);
    if (cached) {
      console.log(`Cache hit for user: ${id}`);
      return c.json(JSON.parse(cached));
    }

    // データベースから取得（ここに直接書く）
    const user = await prisma.user.findUnique({
      where: { id }
    });

    if (!user) {
      return c.json({ error: 'User not found' }, 404);
    }

    // キャッシュに保存（ここに直接書く）
    await redis.set(`user:${id}`, JSON.stringify(user));

    console.log(`Database hit for user: ${id}`);
    return c.json(user);
  } catch (error) {
    console.error('Error fetching user:', error);
    return c.json({ error: 'Internal server error' }, 500);
  }
});

// ユーザー更新API
app.put('/users/:id', async (c) => {
  try {
    const id = c.req.param('id');
    const { name, email } = await c.req.json();

    // バリデーション（同じコードを再度書く）
    if (!name || !email) {
      return c.json({ error: 'Name and email are required' }, 400);
    }
    if (!email.includes('@')) {
      return c.json({ error: 'Invalid email format' }, 400);
    }

    // 存在チェック
    const existingUser = await prisma.user.findUnique({
      where: { id }
    });
    if (!existingUser) {
      return c.json({ error: 'User not found' }, 404);
    }

    // メール重複チェック（他のユーザーとの重複）
    const emailConflict = await prisma.user.findFirst({
      where: {
        email,
        id: { not: id }
      }
    });
    if (emailConflict) {
      return c.json({ error: 'Email already exists' }, 409);
    }

    // ユーザー更新
    const updatedUser = await prisma.user.update({
      where: { id },
      data: { name, email }
    });

    // キャッシュを更新（ここに直接書く）
    await redis.set(`user:${id}`, JSON.stringify(updatedUser));

    console.log(`User updated: ${id}`);
    return c.json(updatedUser);
  } catch (error) {
    console.error('Error updating user:', error);
    return c.json({ error: 'Internal server error' }, 500);
  }
});

// サーバー起動
serve(app, (info) => {
  console.log(`Server running on http://localhost:${info.port}`);
});
```

## このコードの問題点

### 1. **責務の混在**
- HTTPハンドリング、バリデーション、データベース操作、キャッシュ操作、ログ出力が全て混在
- 1つの関数が複数の責務を持っている

### 2. **コードの重複**
- バリデーションロジックが複数箇所に重複
- キャッシュ操作のコードが重複
- エラーハンドリングが重複

### 3. **テストの困難さ**
- データベースやRedisに依存するため、単体テストが困難
- モックやスタブを作成するのが複雑
- 特定の機能だけをテストできない

### 4. **変更の影響範囲が広い**
- データベースをMySQLからPostgreSQLに変更する場合、全てのAPIエンドポイントを修正が必要
- バリデーションルールを変更する場合、複数箇所を修正が必要
- キャッシュ戦略を変更する場合、全てのエンドポイントに影響

### 5. **再利用性の欠如**
- ユーザー作成ロジックをCLIツールで再利用できない
- 他のプロジェクトでユーザー管理機能を再利用できない

### 6. **保守性の低下**
- ファイルが巨大になり、理解が困難
- 新しい機能追加時に既存コードへの影響を把握しにくい
- バグ修正時に副作用を起こしやすい

### 7. **チーム開発の困難さ**
- 複数人で同じファイルを編集するとコンフリクトが発生しやすい
- 機能ごとに担当を分けることができない

## アーキテクチャを導入することで解決される問題

これらの問題は、適切なアーキテクチャ（クリーンアーキテクチャ）を導入することで解決できます：

- **責務の分離**: 各層が明確な責務を持つ
- **再利用性**: ビジネスロジックを複数の場所で再利用可能
- **テスト容易性**: 各層を独立してテスト可能
- **保守性**: 変更の影響範囲を限定できる
- **チーム開発**: 機能ごとにファイルを分割し、並行開発が可能

次のセクションから、これらの問題を解決するクリーンアーキテクチャを詳しく見ていきましょう。

### 3. クリーンアーキテクチャ

テスト容易性、保守性、独立性を極限まで高めることを目指したアーキテクチャである。

#### 3.1. SOLID原則とDI
クリーンアーキテクチャはSOLID原則、特に**依存性反転の原則 (DIP)** と密接に関連している。

参考

https://qiita.com/baby-degu/items/d058a62f145235a0f007

- **S (単一責任の原則: Single Responsibility　Principle)**
クラスは変更の理由が1つだけであるべき。つまり、クラスは1つの責任または関心事だけを持つべきであり、複数の責任を持つとそれぞれの変更に対して他の責任に影響を与える可能性がある。例えば、ユーザー情報の保存とメール送信を両方行うクラスは、責任が複数あるため良くない。なぜなら、メール送信の仕様が変わったときに、ユーザー情報の保存ロジックに影響を与える可能性がある。この原則に従うと、コードの変更がより安全になり、テストも容易になる。
```ts
// 悪い例：複数の責任を持つクラス
class UserManager {
  constructor(private database: Database, private emailService: EmailService) {}

  saveUser(user: User): void {
    // ユーザー情報をデータベースに保存
    this.database.save(user);

    // ウェルカムメールを送信
    const message = `こんにちは、${user.name}さん！`;
    this.emailService.send(user.email, "ご登録ありがとうございます", message);

    // ログを出力
    console.log(`ユーザー ${user.name} が登録されました`);
  }
}

// 良い例：責任が分離されたクラス
class UserRepository {
  constructor(private database: Database) {}

  saveUser(user: User): void {
    this.database.save(user);
  }
}

class UserNotifier {
  constructor(private emailService: EmailService) {}

  sendWelcomeEmail(user: User): void {
    const message = `こんにちは、${user.name}さん！`;
    this.emailService.send(user.email, "ご登録ありがとうございます", message);
  }
}

class Logger {
  logUserRegistration(user: User): void {
    console.log(`ユーザー ${user.name} が登録されました`);
  }
}

// 各クラスを連携させるサービス
class UserService {
  constructor(
    private userRepository: UserRepository,
    private userNotifier: UserNotifier,
    private logger: Logger
  ) {}

  registerUser(user: User): void {
    this.userRepository.saveUser(user);
    this.userNotifier.sendWelcomeEmail(user);
    this.logger.logUserRegistration(user);
  }
}
```

- **O (オープン・クローズドの原則: Open-Closed Principle)**
ソフトウェアエンティティ（クラス、モジュール、関数など）は拡張に対して開かれており、修正に対して閉じているべき。つまり、新しい機能を追加するためにコードを変更するのではなく、既存のコードを変更せずに拡張できるようにするべき。例えば、新しい支払い方法を追加するとき、既存の支払い処理コードを変更するのではなく、新しい支払いクラスを追加するだけで済むようにする。この原則に従うと、既存のコードに対する影響なく、アプリケーションに新しい機能を追加できるようになる。

```ts
// 悪い例：新しい形を追加するたびにクラスを修正する必要がある
class AreaCalculator {
  calculateArea(shape: any): number {
    if (shape.type === 'circle') {
      return Math.PI * shape.radius * shape.radius;
    } else if (shape.type === 'rectangle') {
      return shape.width * shape.height;
    }
    // 新しい形を追加するたびにこのメソッドを変更する必要がある
    return 0;
  }
}

// 良い例：インターフェースとポリモーフィズムを使用
interface Shape {
  calculateArea(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}

  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  calculateArea(): number {
    return this.width * this.height;
  }
}

// 新しい形を追加しても、AreaCalculatorを変更する必要はない
class Triangle implements Shape {
  constructor(
    private base: number,
    private height: number
  ) {}

  calculateArea(): number {
    return (this.base * this.height) / 2;
  }
}

class AreaCalculator {
  calculateArea(shape: Shape): number {
    return shape.calculateArea();
  }
}
```

- **L (リスコフの置換原則: Liskov Substitution Principle)**

サブタイプ（派生クラス）は、そのスーパータイプ（基底クラス）と置換可能でなければならない。つまり、派生クラスはそのベースクラスの代わりに使用できるべきであり、派生クラスの挙動がベースクラスの挙動と矛盾してはならない。例えば、`Bird`クラスを継承した`Penguin`クラスが`fly()`メソッドをオーバーライドして例外をスローするのは、この原則に違反する。なぜならペンギンは飛べないので、`Bird`の代わりに`Penguin`を使うとプログラムが正しく動作しなくなるからである。この原則に従うことで、継承関係がより明確になり、コードの堅牢性が高まる。
```ts
// 悪い例：派生クラスがベースクラスの前提条件を壊している
class Rectangle {
  constructor(protected width: number, protected height: number) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }

  // これはLSPに違反している：正方形は幅を変えると高さも変わる
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // 高さも変更してしまう
  }

  setHeight(height: number): void {
    this.height = height;
    this.width = height; // 幅も変更してしまう
  }
}

// 問題の例：
function calculateAreaWithWidthIncrease(rectangle: Rectangle): number {
  // 長方形の幅を5増やす
  const originalWidth = rectangle.getWidth();
  rectangle.setWidth(originalWidth + 5);

  // 高さは変わらないと仮定している
  return rectangle.getArea();
}

// 長方形の場合：期待通りに動作する
const rect = new Rectangle(5, 10);
calculateAreaWithWidthIncrease(rect); // 期待: (5+5) * 10 = 100

// 正方形の場合：予期せぬ結果になる
const square = new Square(5);
calculateAreaWithWidthIncrease(square); // 期待外: (5+5) * (5+5) = 100（高さも変わっている）

// 良い例：継承を使わず、明示的にクラスを分ける
interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(private size: number) {}

  setSize(size: number): void {
    this.size = size;
  }

  getArea(): number {
    return this.size * this.size;
  }
}
```

- **I (インターフェース分離の原則: Interface Segregation Principle)**
クライアントは、使用しないメソッドに依存すべきではない。つまり、大きくて太ったインターフェースを複数の小さなインターフェースに分割すべきという考え方。例えば、プリンタとスキャナーの機能をすべて含む大きなインターフェースがあると、プリンタだけを実装するクラスはスキャン関連のメソッドも実装しなければならない。この原則に従って、機能ごとに小さなインターフェースに分割することで、クラスは必要な機能だけを実装でき、コードの柔軟性が高まる。

```ts
// 悪い例：太ったインターフェース
interface WorkerMachine {
  print(document: Document): void;
  scan(document: Document): Image;
  fax(document: Document): void;
  staple(documents: Document[]): void;
}

// 問題点：すべてのプリンタがスキャンやファックスをサポートしているわけではない
class SimplePrinter implements WorkerMachine {
  print(document: Document): void {
    // 印刷機能の実装
  }

  scan(document: Document): Image {
    // この機能はサポートしていないが、実装しなければならない
    throw new Error("スキャン機能はサポートしていません");
  }

  fax(document: Document): void {
    // この機能はサポートしていないが、実装しなければならない
    throw new Error("ファックス機能はサポートしていません");
  }

  staple(documents: Document[]): void {
    // この機能はサポートしていないが、実装しなければならない
    throw new Error("ステープル機能はサポートしていません");
  }
}

// 良い例：役割ごとに分割されたインターフェース
interface Printer {
  print(document: Document): void;
}

interface Scanner {
  scan(document: Document): Image;
}

interface FaxMachine {
  fax(document: Document): void;
}

interface Stapler {
  staple(documents: Document[]): void;
}

// 各クラスは必要なインターフェースだけを実装する
class SimplePrinter implements Printer {
  print(document: Document): void {
    // 印刷機能の実装
  }
}

// 多機能な機器は複数のインターフェースを実装できる
class MultiFunctionPrinter implements Printer, Scanner, FaxMachine {
  print(document: Document): void {
    // 印刷機能の実装
  }

  scan(document: Document): Image {
    // スキャン機能の実装
    return new Image();
  }

  fax(document: Document): void {
    // ファックス機能の実装
  }
}

class ProfessionalPrinter implements Printer, Stapler {
  print(document: Document): void {
    // 印刷機能の実装
  }

  staple(documents: Document[]): void {
    // ステープル機能の実装
  }
}
```

- **D (依存性反転の原則: Dependency Inversion Principle)**
上位モジュールは下位モジュールに依存すべきでなく、両者は抽象（インターフェースやアブストラクトクラス）に依存すべき。抽象は詳細に依存すべきでなく、詳細が抽象に依存すべき。例えば、高レベルのビジネスロジックが直接MySQLデータベースに依存するのではなく、データアクセスインターフェースに依存し、そのインターフェースの実装としてMySQLリポジトリを使用する。この原則に従うと、データベースをMySQLからMongoDBに変更したい場合、高レベルのビジネスロジックを変更せずに、新しいMongoDBリポジトリを実装するだけで済む。

```ts
// 悪い例：上位モジュールが下位モジュールに直接依存している
class MySQLUserRepository {
  getUser(id: string): User {
    // MySQLからユーザーを取得する実装
    return new User(id, "ユーザー名");
  }
}

class UserService {
  constructor(private userRepository: MySQLUserRepository) {}

  getUserById(id: string): User {
    return this.userRepository.getUser(id);
  }
}

// 問題点：UserServiceはMySQLUserRepositoryに直接依存している
// 異なるデータベースに切り替えたい場合、UserServiceも変更する必要がある

// 良い例：上位モジュールと下位モジュールが共に抽象に依存している
interface UserRepository {
  getUser(id: string): User;
}

class MySQLUserRepository implements UserRepository {
  getUser(id: string): User {
    // MySQLからユーザーを取得する実装
    return new User(id, "ユーザー名");
  }
}

class MongoDBUserRepository implements UserRepository {
  getUser(id: string): User {
    // MongoDBからユーザーを取得する実装
    return new User(id, "ユーザー名");
  }
}

class UserService {
  constructor(private userRepository: UserRepository) {}

  getUserById(id: string): User {
    return this.userRepository.getUser(id);
  }
}

// 依存性注入によって、どのリポジトリ実装を使用するか決定できる
const mySQLRepo = new MySQLUserRepository();
const mongoRepo = new MongoDBUserRepository();

// MySQLを使用する場合
const userServiceWithMySQL = new UserService(mySQLRepo);

// MongoDBに切り替える場合（UserServiceのコードは変更不要）
const userServiceWithMongo = new UserService(mongoRepo);

// テストで使用するモックリポジトリ
class MockUserRepository implements UserRepository {
  getUser(id: string): User {
    return new User(id, "モックユーザー");
  }
}

// テストでモックリポジトリを注入
const userServiceForTest = new UserService(new MockUserRepository());
```

:::note info
**サブタイプとそのスーパータイプ**

サブタイプとスーパータイプは、オブジェクト指向プログラミングにおける継承関係を表します。

- **スーパータイプ（親クラス・基底クラス）**: より一般的で抽象的な概念を表すクラスやインターフェース
- **サブタイプ（子クラス・派生クラス）**: スーパータイプを継承し、より具体的な実装を持つクラス

例：
```typescript
// スーパータイプ（抽象的）
interface Animal {
  makeSound(): string;
}

// サブタイプ（具体的）
class Dog implements Animal {
  makeSound(): string {
    return "ワンワン";
  }
}

class Cat implements Animal {
  makeSound(): string {
    return "ニャーニャー";
  }
}
```

**上位(抽象)モジュールと下位(具体)モジュールとは？**

これは依存性反転の原則（DIP）における重要な概念です。

**上位(抽象)モジュールとは?**

- **ビジネスロジックや重要な処理を担当**するモジュール
- **「何をするか」を定義**するが、「どうやってするか」の詳細は知らない
- より安定しており、変更されにくい
- 抽象（インターフェース）に依存する

例：本アプリケーションでは
- `UserUsecase`（ユーザー作成・取得のビジネスロジック）
- `UserController`（HTTPリクエストの処理ロジック）

**下位(具体)モジュールとは?**

- **技術的な詳細や実装方法を担当**するモジュール
- **「どうやってするか」を具体的に実装**する
- 変更されやすい（データベースの変更、外部APIの変更など）
- 具体的な技術に依存する

例：本アプリケーションでは
- `MysqlUserRepository`（MySQLでのデータ保存方法）
- `RedisUserRepository`（Redisでのデータ保存方法）
- `PrismaClient`（Prismaを使ったDB接続方法）

**従来の問題のある依存関係：**
```
上位モジュール → 下位モジュール
（ビジネスロジック） → （MySQL実装）
```
この場合、MySQLからPostgreSQLに変更したい時、ビジネスロジックも変更が必要になってしまいます。

**依存性反転後の理想的な依存関係：**
```
上位モジュール → インターフェース ← 下位モジュール
（ビジネスロジック） → （Repository抽象） ← （MySQL実装）
```
この場合、MySQL実装をPostgreSQL実装に差し替えても、ビジネスロジックは一切変更不要です。

:::

- **DI (依存性注入: Dependency Injection):**
依存性反転を実現するための具体的なテクニックである。クラスが必要とするオブジェクト（依存オブジェクト）を、クラスの外部から与える（注入する）設計パターンである。これにより、クラスは具体的な実装ではなく、抽象（インターフェース）にのみ依存できる。

今回の実装では、`index.ts`がDIコンテナの役割を果たし、`Usecase`に`Repository`の具体的な実装（例: `MysqlUserRepository`）を注入する。

#### 3.2. クリーンアーキテクチャの各層の責務

クリーンアーキテクチャは、システムを複数の層に分けて設計する手法です。
レストランで例えると、**役割ごとに部門を分けて**、それぞれが専門性を持って働く組織構造のようなものです。

**各層の役割：**
- **🔵 内側（ドメイン）**: 変更されにくい核となる部分
- **🟢 中間層（ビジネスロジック）**: ビジネスロジックを組み合わせた業務フロー
- **🟡 外側（技術的詳細）**: 変更されやすい技術的な実装部分

各層での責務を詳しく記載します。

**🔵 内側（ドメイン）**:
- **src/domain/entity**
    - アプリケーションで扱うデータ型の単位。このデータ型を基にロジックを組んでいく
- **src/domain/repository**
    - データ通信をするための抽象メソッドの定義をするところ。実装は`src/infrastructure/repository`に存在する

 **🟢 中間層（ビジネスロジック）**:
- **src/usecase**
    - 実際のアプリケーションのビジネスロジックを定義します

**🟡 外側（技術的詳細）**:
- **src/interface/handler**
    - クライアントや他マイクロサービスに通信する方法を定義する場所
- **src/infrastructure/repository**
    - データベースに接続したり、SQLでデータベース操作をする実装をするところ

```mermaid
graph TB
    subgraph "外側（技術的詳細）"
        Infra[Infrastructure<br/>repository<br/>例:MySQL実装]
        Interface[Interface<br/>handler<br/>例:Express.jsハンドラー]
    end

    subgraph "中間層（ビジネスロジック）"
        UseCase[Use Case<br/>usecase]
    end

    subgraph "内側（ドメイン）"
        Repo[Repository Interface<br/>domain/repository]
        Domain[Domain<br/>domain/entity]
    end

    %% 依存関係（矢印の方向が依存の方向）
    Infra -->|実装| Repo
    Infra -->|使用| Domain
    UseCase -->|使用| Repo
    UseCase -->|使用| Domain
    Interface -->|呼び出し| UseCase

    %% スタイリング
    classDef outerLayer fill:#ffeb3b,stroke:#f57f17,stroke-width:2px
    classDef middleLayer fill:#4caf50,stroke:#2e7d32,stroke-width:2px
    classDef innerLayer fill:#2196f3,stroke:#1565c0,stroke-width:2px

    class Infra,Interface outerLayer
    class UseCase middleLayer
    class Repo,Domain innerLayer
```

**クリーンアーキテクチャの恩恵**:
1.  **フレームワーク非依存**
UI、データベース、Webフレームワークなどを容易に変更できる。
2.  **テスト容易性**
ビジネスロジックを技術的詳細から分離することで、単体テストが格段に書きやすくなります
3.  **UI非依存**
同じビジネスロジックを、Web API、CLI ツール、バッチ処理などで再利用できます
4.  **データベース非依存**
データベースの種類やORMをビジネスロジックに影響を与えることなく変更できる。


#### 3.3. ディレクトリ構造と作成コマンド

```bash
.
├── prisma
│   └── schema.prisma
└── src
    ├── domain
    │   ├── entity
    │   │   └── user.ts
    │   └── repository
    │       └── userRepository.ts
    ├── infrastructure
    │   ├── mysql
    │   │   ├── mysqlUserRepository.ts
    │   │   └── prismaClient.ts
    │   └── redis
    │       ├── redisClient.ts
    │       └── redisUserRepository.ts
    ├── interface
    │   ├── dto
    │   │   └── userDto.ts
    │   └── handler
    │       └── userHandler.ts
    ├── usecase
    │   └── userUsecase.ts
    ├── cli.ts
    ├── index.ts
    └── router.ts
```
```bash
mkdir -p src/domain/entity src/infrastructure/mysql src/infrastructure/redis src/interface/dto src/interface/handler src/domain/repository src/usecase
touch src/domain/entity/user.ts src/infrastructure/mysql/prismaClient.ts src/infrastructure/mysql/mysqlUserRepository.ts src/infrastructure/redis/redisClient.ts src/infrastructure/redis/redisUserRepository.ts src/interface/dto/userDto.ts src/interface/handler/userHandler.ts src/domain/repository/userRepository.ts src/usecase/userUsecase.ts src/cli.ts src/index.ts src/router.ts
```

### 3.4. コード例

#### Domain Layer
ドメイン層は、アプリケーションの中心に位置し、ビジネスロジックと業務ルールをカプセル化した、技術的詳細に依存しない核心部分である

**`src/domain/entity/user.ts` (Entity)**
ビジネスの核となるデータと振る舞いを持つオブジェクト。識別子を持ち、ビジネスルールをカプセル化する。データベースやフレームワークから独立して存在する純粋なドメインモデル。
```src/domain/entity/user.ts
// Userエンティティの定義
// ビジネスの核となるUserオブジェクトのインターフェース

// Userエンティティのプロパティを定義するインターフェース
export interface User {
  id: string;
  name: string;
  email: string;
}
```

**`src/domain/repository/userRepository.ts` (Interface / Port)**
エンティティの保存と取得を抽象化するインターフェース。データストレージの詳細を隠蔽し、コレクションのようにエンティティを操作できるようにする。実装はインフラ層で行われるが、インターフェースはドメイン層に属する。
```src/domain/repository/userRepository.ts
// UserRepositoryインターフェースの定義
// Userエンティティのデータ永続化操作に関する「契約」を定義する
import type { User } from '@/domain/entity/user';

// ユーザーリポジトリのインターフェース
export interface IUserRepository {
  // 指定されたIDを持つユーザーを検索する
  findById(id: string): Promise<User | null>;

  // 指定されたメールアドレスを持つユーザーを検索する
  findByEmail(email: string): Promise<User | null>;

  // 新しいユーザーを作成して永続化層に保存する
  create(data: Omit<User, 'id'>): Promise<User>;

  // 既存のユーザー情報を永続化層で更新する
  update(user: User): Promise<User | null>;
  // save(user: User): Promise<User>; // createとupdateを兼ねる場合、このようなシグネチャも考えられる

  // 指定されたIDを持つユーザーを削除する
  deleteById(id: string): Promise<boolean>;

  // すべてのユーザーを取得する
  findAll(): Promise<User[]>;
}
```

#### Usecase Layer
ユースケース層とは、アプリケーションの具体的な機能やビジネスロジックを実装し、ドメイン層のエンティティを操作して特定のシナリオを実現する中心的な層

**`src/usecase/userUsecase.ts`**

```src/usecase/userUsecase.ts
// UserUsecaseクラスの定義
// ユーザーに関連するアプリケーションのユースケース（ビジネスロジック）を実装する
import type { User } from '@/domain/entity/user';
import type { IUserRepository } from '@/domain/repository/userRepository';
import type { CreateUserDto, UpdateUserDto, UserDto } from '@/interface/dto/userDto';

// ユーザーユースケースのインターフェース(ハンドラーそうで呼び出すもの)
export interface IUserUsecase {
  // 指定されたIDのユーザーを取得する
  getUserById(id: string): Promise<UserDto | null>;

  // 新しいユーザーを作成する
  createUser(dto: CreateUserDto): Promise<UserDto>;

  // すべてのユーザーを取得する
  getAllUsers(): Promise<UserDto[]>;

  // ユーザー情報を更新する
  updateUser(id: string, dto: UpdateUserDto): Promise<UserDto | null>;

  // ユーザーを削除する
  deleteUser(id: string): Promise<boolean>;
}

// UserUsecaseクラス。IUserUsecaseインターフェースを実装する
export class UserUsecase implements IUserUsecase {
  // UserUsecaseのコンストラクタ
  constructor(private readonly userRepository: IUserRepository) {}

  // UserエンティティをUserDtoに変換するプライベートメソッド
  private toDto(user: User): UserDto {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
    };
  }

  // 指定されたIDのユーザーを取得する
  async getUserById(id: string): Promise<UserDto | null> {
    const user = await this.userRepository.findById(id);
    return user ? this.toDto(user) : null;
  }

  // 新しいユーザーを作成する
  async createUser(dto: CreateUserDto): Promise<UserDto> {
    const existingUser = await this.userRepository.findByEmail(dto.email);
    if (existingUser) {
      throw new Error(`Business Rule Violation: User with email ${dto.email} already exists.`);
    }

    // リポジトリのcreateメソッドを使用して永続化とID生成を行う
    const newUserEntity = await this.userRepository.create({
      name: dto.name,
      email: dto.email,
    });

    return this.toDto(newUserEntity);
  }

  // すべてのユーザーを取得する
  async getAllUsers(): Promise<UserDto[]> {
    const users = await this.userRepository.findAll();
    return users.map(user => this.toDto(user));
  }

  // ユーザー情報を更新する
  async updateUser(id: string, dto: UpdateUserDto): Promise<UserDto | null> {
    const existingUser = await this.userRepository.findById(id);
    if (!existingUser) {
      return null; // Or throw a specific "NotFound" error
    }

    // メールアドレスが変更され、かつ新しいメールアドレスが他で使用されているかチェック
    if (dto.email && dto.email !== existingUser.email) {
      const userWithNewEmail = await this.userRepository.findByEmail(dto.email);
      if (userWithNewEmail && userWithNewEmail.id !== id) {
        throw new Error(`Business Rule Violation: Email ${dto.email} is already in use by another user.`);
      }
    }

    const userToUpdate: User = {
      id: existingUser.id, // IDは変更しない
      name: dto.name ?? existingUser.name, // 提供されていなければ既存の値を使用
      email: dto.email ?? existingUser.email, // 提供されていなければ既存の値を使用
    };

    const updatedUser = await this.userRepository.update(userToUpdate);
    return updatedUser ? this.toDto(updatedUser) : null;
  }

  // ユーザーを削除する
  async deleteUser(id: string): Promise<boolean> {
    // 事前にユーザーが存在するか確認することも可能
    // リポジトリのdeleteByIdが成否を返す契約ならそれに従う
    return this.userRepository.deleteById(id);
  }
}
```

#### Infrastructure Layer
インフラストラクチャ層は、データベースアクセスや外部APIとの連携など、技術的な実装の詳細を担当し、ドメイン層で定義されたリポジトリインターフェースの具体的な実装を提供する

[Prismaの設定ファイルはこちら(2. 環境構築)へ](#2-環境構築)

**`src/infrastructure/mysql/prismaClient.ts`**
PrismaClientのシングルトンインスタンスを提供するモジュール。
prismaに接続するためのファイル。
```src/infrastructure/mysql/prismaClient.ts
// PrismaClientのシングルトンインスタンスを提供するモジュール
import { PrismaClient } from '@prisma/client';

// PrismaClientのシングルトンインスタンス
export const prisma = new PrismaClient({
  // log: ['query', 'info', 'warn', 'error'], // 開発時に有効化
});

// PrismaClientの接続を安全に切断する関数
export async function disconnectPrisma(): Promise<void> {
  await prisma.$disconnect();
}
```

**`src/infrastructure/mysql/mysqlUserRepository.ts`**
prismaを用いて実際にデータ通信部分を実装するファイル。
```src/infrastructure/mysql/mysqlUserRepository.ts
// MysqlUserRepositoryクラスの定義
// IUserRepositoryインターフェースをMySQL (Prisma ORM) を使用して実装する
import type { IUserRepository } from '@/domain/repository/userRepository';
import type { User } from '@/domain/entity/user';
import { prisma } from './prismaClient';
import type { User as PrismaUser } from '@prisma/client'; // Prismaが生成する型
import { v4 as uuidv4 } from 'uuid'; // ID生成用

/**
 * MysqlUserRepositoryクラス。IUserRepositoryインターフェースを実装する。
 */
export class MysqlUserRepository implements IUserRepository {
  // Prisma UserモデルをドメインのUserモデルに変換する
  private toDomainModel(prismaUser: PrismaUser): User {
    return {
      id: prismaUser.id,
      name: prismaUser.name,
      email: prismaUser.email,
    };
  }

  // IDでユーザーを取得する
  async findById(id: string): Promise<User | null> {
    const prismaUser = await prisma.user.findUnique({
      where: { id },
    });
    return prismaUser ? this.toDomainModel(prismaUser) : null;
  }

  // メールアドレスでユーザーを取得する
  async findByEmail(email: string): Promise<User | null> {
    const prismaUser = await prisma.user.findUnique({
      where: { email },
    });
    return prismaUser ? this.toDomainModel(prismaUser) : null;
  }

  // 新しいユーザーを作成する
  async create(data: Omit<User, 'id'>): Promise<User> {
    const newPrismaUser = await prisma.user.create({
      data: {
        id: uuidv4(), // Prismaスキーマで @default(uuid()) がある場合、この行は不要になることがある
        name: data.name,
        email: data.email,
        // createdAt, updatedAt はスキーマから削除されたため、ここでは指定しない
      },
    });
    return this.toDomainModel(newPrismaUser);
  }

  // ユーザー情報を更新する
  async update(user: User): Promise<User | null> {
    try {
      const updatedPrismaUser = await prisma.user.update({
        where: { id: user.id },
        data: {
          name: user.name,
          email: user.email,
          // updatedAt はスキーマから削除されたため、Prismaは自動更新しない
        },
      });
      return this.toDomainModel(updatedPrismaUser);
    } catch (error: unknown) {
      if (typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'P2025') {
        console.warn(`User with ID ${user.id} not found for update.`);
        return null;
      }
      console.error('Error updating user in MysqlUserRepository:', error);
      throw error; // その他の予期せぬエラーは再スロー
    }
  }

  // IDでユーザーを削除する
  async deleteById(id: string): Promise<boolean> {
    try {
      await prisma.user.delete({
        where: { id },
      });
      return true;
    } catch (error: any) {
      if (error.code === 'P2025') { // Record to delete does not exist
        return false; // 対象が存在しない場合は false を返す
      }
      console.error('Error deleting user by ID in MysqlUserRepository:', error);
      throw error; // その他のエラーは再スロー
    }
  }

  // 全ユーザーを取得する
  async findAll(): Promise<User[]> {
    const prismaUsers = await prisma.user.findMany();
    return prismaUsers.map(user => this.toDomainModel(user));
  }
}
```

**`src/infrastructure/redis/redisClient.ts`**
Redisクライアントのインスタンスを提供するモジュール。
redisに接続するファイル。
```src/infrastructure/redis/redisClient.ts
/**
 * @file Redisクライアントのインスタンスを提供するモジュール。
 */
import { createClient, type RedisClientType } from 'redis';

let redisClientInstance: RedisClientType | null = null;

/**
 * Redisクライアントのシングルトンインスタンスを取得または作成する。
 * @returns RedisClientTypeのインスタンス。
 * @throws Error Redisへの接続に失敗した場合。
 */
export async function getRedisClient(): Promise<RedisClientType> {
  if (redisClientInstance && redisClientInstance.isOpen) {
    return redisClientInstance;
  }

  const redisHost = process.env.REDIS_HOST || '127.0.0.1';
  const redisPort = process.env.REDIS_PORT || '6379';
  const redisURL = `redis://${redisHost}:${redisPort}`;

  const client = createClient({ url: redisURL });

  client.on('error', (err) => console.error('Redis Client Error', err));
  // 他のイベントリスナーも必要に応じて追加

  try {
    if (!client.isOpen) {
      await client.connect();
    }
    redisClientInstance = client as unknown as RedisClientType;
    return redisClientInstance;
  } catch (err) {
    console.error('Failed to connect to Redis:', err);
    throw err;
  }
}

/**
 * Redisクライアントの接続を安全に切断する関数。
 */
export async function disconnectRedis(): Promise<void> {
  if (redisClientInstance && redisClientInstance.isOpen) {
    await redisClientInstance.quit();
    redisClientInstance = null;
  }
}

```

**`src/infrastructure/redis/redisUserRepository.ts`**
redisとデータ通信するための具体実装をするファイル
```src/infrastructure/redis/redisUserRepository.ts
/**
 * @file RedisUserRepositoryクラスの定義
 * @description IUserRepositoryインターフェースをRedisを使用して実装する。
 * タイムスタンプフィールドが除外されたUserエンティティに対応。
 */
import type { IUserRepository } from '@/domain/repository/userRepository';
import type { User } from '@/domain/entity/user';
import { getRedisClient } from './redisClient';
import type { RedisClientType } from 'redis';
import { v4 as uuidv4 } from 'uuid';

const USER_KEY_PREFIX = 'user:';

/**
 * RedisUserRepositoryクラス。IUserRepositoryインターフェースを実装する。
 */
export class RedisUserRepository implements IUserRepository {
  private redisClient!: RedisClientType;

  // Redisクライアントを確実に取得するためのヘルパーメソッド
  private async getClient(): Promise<RedisClientType> {
    if (!this.redisClient || !this.redisClient.isOpen) {
      this.redisClient = await getRedisClient();
    }
    return this.redisClient;
  }

  // IDでユーザーを取得する
  async findById(id: string): Promise<User | null> {
    const client = await this.getClient();
    const data = await client.get(`<span class="math-inline">\{USER\_KEY\_PREFIX\}</span>{id}`);
    return data ? (JSON.parse(data) as User) : null;
  }

  // メールアドレスでユーザーを取得する
  async findByEmail(email: string): Promise<User | null> {
    const client = await this.getClient();
    // 注意: このemail検索はRedisの基本機能だけでは非効率
    // 効率化にはセカンダリインデックスの仕組みが必要
    // ここではデモのため、全キーを走査するような非効率な方法を示唆するが、実際には避けるべき
    console.warn(
      'RedisUserRepository.findByEmail is NOT EFFICIENT for production without secondary indexing.'
    );
    const keys = await client.keys(`${USER_KEY_PREFIX}*`);
    for (const key of keys) {
      const data = await client.get(key);
      if (data) {
        const user = JSON.parse(data) as User;
        if (user.email === email) {
          return user;
        }
      }
    }
    return null;
  }

  // 新しいユーザーを作成する
  async create(data: Omit<User, 'id'>): Promise<User> {
    const client = await this.getClient();
    const user: User = {
      id: uuidv4(),
      name: data.name,
      email: data.email,
    };
    await client.set(`<span class="math-inline">\{USER\_KEY\_PREFIX\}</span>{user.id}`, JSON.stringify(user));
    return user;
  }

  // ユーザー情報を更新する
  async update(user: User): Promise<User | null> {
    const client = await this.getClient();
    // Redisで更新する場合、対象が存在するかどうかを先に確認することが推奨される。
    const existingData = await client.get(`<span class="math-inline">\{USER\_KEY\_PREFIX\}</span>{user.id}`);
    if (!existingData) {
      console.warn(`User with ID ${user.id} not found in Redis for update.`);
      return null; // 対象が存在しない場合はnullを返す
    }
    await client.set(`<span class="math-inline">\{USER\_KEY\_PREFIX\}</span>{user.id}`, JSON.stringify(user));
    return user;
  }

  // IDでユーザーを削除する
  async deleteById(id: string): Promise<boolean> {
    const client = await this.getClient();
    const result = await client.del(`<span class="math-inline">\{USER\_KEY\_PREFIX\}</span>{id}`);
    return result > 0; // 1つ以上のキーが削除されれば成功
  }

  // 全ユーザーを取得する
  async findAll(): Promise<User[]> {
    const client = await this.getClient();
    const keys = await client.keys(`${USER_KEY_PREFIX}*`);
    if (keys.length === 0) {
      return [];
    }
    // MGETを使うと複数のキーを一度に取得できる
    const results = await client.mGet(keys);
    return results
      .filter((data): data is string => data !== null) // null (キーが存在しない) をフィルタリング
      .map((data) => JSON.parse(data) as User);
  }
}
```

#### Interface Adapter Layer
インターフェース層は、外部システムとビジネスロジック（ユースケース層）を橋渡しするアダプターの役割を担う。外部データを内部モデルに変換し、依存関係が内側に向くよう制御する。

**`src/interface/dto/userDto.ts`**
APIの入出力やユースケース間でのデータ転送に使用されるオブジェクトの型を定義するファイル。
```src/interface/dto/userDto.ts
// UserDTO (Data Transfer Object) の定義
// APIの入出力やユースケース間でのデータ転送に使用されるオブジェクトの型を定義する
import { z } from 'zod';

// ユーザー作成時の入力データに対するバリデーションスキーマ
export const CreateUserDtoSchema = z.object({
  name: z.string().min(1, { message: 'Name is required.' }).max(100),
  email: z.string().email({ message: 'Invalid email address.' }),
});
export type CreateUserDto = z.infer<typeof CreateUserDtoSchema>;

// ユーザー更新時の入力データに対するバリデーションスキーマ
export const UpdateUserDtoSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  email: z.string().email().optional(),
});
export type UpdateUserDto = z.infer<typeof UpdateUserDtoSchema>;

// APIレスポンスやユースケースの出力として使用されるユーザー情報のDTO
export interface UserDto {
  id: string;
  name: string;
  email: string;
}
```

**`src/interface/handler/userHandler.ts`**
Honoフレームワークのコンテキストを受け取り、HTTPリクエストの処理、バリデーション、ユースケースの呼び出し、HTTPレスポンスの生成を担当する。
```src/interface/handler/userHandler.ts
// UserHandlerクラスの定義
// Honoフレームワークのコンテキストを受け取り、HTTPリクエストの処理を担当する
import type { Context } from 'hono';
import type { IUserUsecase } from '@/usecase/userUsecase';
import { CreateUserDtoSchema, UpdateUserDtoSchema } from '@/interface/dto/userDto';

// UserHandlerクラス
export class UserHandler {
  constructor(private readonly userUsecase: IUserUsecase) {}

  // 新しいユーザーを作成するハンドラーメソッド
  async createUser(c: Context): Promise<Response> {
    try {
      const body = await c.req.json();
      const validationResult = CreateUserDtoSchema.safeParse(body);

      if (!validationResult.success) {
        return c.json({ message: 'Invalid input', errors: validationResult.error.flatten().fieldErrors }, 400);
      }

      const createdUser = await this.userUsecase.createUser(validationResult.data);
      return c.json(createdUser, 201);
    } catch (error: unknown) {
      // エラーハンドリングの詳細は前回回答を参照
      const message = error instanceof Error ? error.message : 'An unexpected error occurred';
      if (message.includes('already exists')) {
        return c.json({ message }, 409);
      }
      console.error('Error in createUser handler:', error);
      return c.json({ message: 'Failed to create user', error: message }, 500);
    }
  }

  // 指定されたIDのユーザーを取得するハンドラーメソッド
  async getUserById(c: Context): Promise<Response> {
    try {
      const id = c.req.param('id');
      const user = await this.userUsecase.getUserById(id);
      if (!user) {
        return c.json({ message: 'User not found.' }, 404);
      }
      return c.json(user, 200);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : 'An unexpected error occurred';
      console.error('Error in getUserById handler:', error);
      return c.json({ message: 'Failed to get user', error: message }, 500);
    }
  }

  // 全ユーザーを取得するハンドラーメソッド
  async getAllUsers(c: Context): Promise<Response> {
    try {
      const users = await this.userUsecase.getAllUsers();
      return c.json(users, 200);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : 'An unexpected error occurred';
      console.error('Error in getAllUsers handler:', error);
      return c.json({ message: 'Failed to get all users', error: message }, 500);
    }
  }

  // ユーザー情報を更新するハンドラーメソッド
  async updateUser(c: Context): Promise<Response> {
    try {
      const id = c.req.param('id');
      const body = await c.req.json();
      const validationResult = UpdateUserDtoSchema.safeParse(body);

      if (!validationResult.success) {
        return c.json({ message: 'Invalid input', errors: validationResult.error.flatten().fieldErrors }, 400);
      }
      if (Object.keys(validationResult.data).length === 0) {
        return c.json({ message: 'No update fields provided.' }, 400);
      }

      const updatedUser = await this.userUsecase.updateUser(id, validationResult.data);
      if (!updatedUser) {
        return c.json({ message: 'User not found or no update occurred.' }, 404);
      }
      return c.json(updatedUser, 200);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : 'An unexpected error occurred';
      if (message.includes('already in use')) {
        return c.json({ message }, 409);
      }
      console.error('Error in updateUser handler:', error);
      return c.json({ message: 'Failed to update user', error: message }, 500);
    }
  }

  // ユーザーを削除するハンドラーメソッド
  async deleteUser(c: Context): Promise<Response> {
    try {
      const id = c.req.param('id');
      const success = await this.userUsecase.deleteUser(id);
      if (!success) {
        return c.json({ message: 'User not found or failed to delete.' }, 404);
      }
      return c.json({ message: 'User deleted successfully.' }, 200);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : 'An unexpected error occurred';
      console.error('Error in deleteUser handler:', error);
      return c.json({ message: 'Failed to delete user', error: message }, 500);
    }
  }
}

```

#### Application Entry Points & Router
アプリケーションのエントリーポイントとルーティングを設定する。

**`src/router.ts`**
ルーティングの設定
```src/router.ts
/**
 * @file APIルーターのセットアップを行うモジュール。
 */
import { Hono } from 'hono';
import type { UserHandler } from '@/interface/handler/userHandler';

/**
 * UserHandlerのルートをHonoアプリケーションにセットアップする。
 * @param userHandler - UserHandlerのインスタンス。
 * @returns ルートが登録されたHonoアプリケーションインスタンス。
 */
export function setupUserRoutes(userHandler: UserHandler): Hono {
  const userRouter = new Hono();

  userRouter.get('/', (c) => userHandler.getAllUsers(c));
  userRouter.post('/', (c) => userHandler.createUser(c));

  userRouter.get('/:id', (c) => userHandler.getUserById(c));
  userRouter.put('/:id', (c) => userHandler.updateUser(c));
  userRouter.delete('/:id', (c) => userHandler.deleteUser(c));

  return userRouter;
}
```

**`src/index.ts` (Web APIエントリーポイント / Composition Root)**
アプリケーションのエントリーポイントを定義
```src/index.ts
/**
 * @file アプリケーションのメインエントリーポイント (Web API)。
 */
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { UserUsecase } from '@/usecase/userUsecase';
import { UserHandler } from '@/interface/handler/userHandler';
import { setupUserRoutes } from '@/router';
import { MysqlUserRepository } from '@/infrastructure/mysql/mysqlUserRepository';
import { RedisUserRepository } from '@/infrastructure/redis/redisUserRepository';
import { disconnectPrisma } from '@/infrastructure/mysql/prismaClient';
import { disconnectRedis, getRedisClient } from '@/infrastructure/redis/redisClient';
import type { IUserRepository } from '@/domain/repository/userRepository';

async function bootstrap() {
  const USE_REDIS_FOR_USERS = process.env.USE_USER_REPO === 'redis';
  let userRepository: IUserRepository;

  if (USE_REDIS_FOR_USERS) {
    try {
      await getRedisClient();
      userRepository = new RedisUserRepository();
      console.log('Using RedisUserRepository for API.');
    } catch (redisError) {
      console.error('API: Failed to connect to Redis, falling back to MySQL:', redisError);
      userRepository = new MysqlUserRepository();
    }
  } else {
    userRepository = new MysqlUserRepository();
    console.log('Using MySQLUserRepository for API.');
  }

  const userUsecase = new UserUsecase(userRepository);
  const userHandler = new UserHandler(userUsecase);
  const app = new Hono();

  app.use('*', async (c, next) => { /* ... logging middleware ... */ await next(); });
  const userRouter = setupUserRoutes(userHandler);
  app.route('/api/users', userRouter);
  app.get('/', (c) => c.text('Clean Architecture API (User Simple)'));
  app.onError((err, c) => { /* ... error handling ... */ return c.json({ message: err.message }, 500);});

  const port = parseInt(process.env.PORT || '3000');
  serve({ fetch: app.fetch, port }, (info) => console.log(`API Server listening on http://localhost:${info.port}`));

  process.on('SIGINT', async () => {
    console.log('\nGracefully shutting down...');
    await disconnectPrisma();
    await disconnectRedis();
    process.exit(0);
  });
  process.on('SIGTERM', async () => {
    console.log('\nGracefully shutting down...');
    await disconnectPrisma();
    await disconnectRedis();
    process.exit(0);
  });
}

bootstrap().catch((error) => {
  console.error('Failed to bootstrap API:', error);
  process.exit(1);
});

```
### 4. アプリケーションの実行方法

#### 4.1. データベースの準備
1. `bun.lock`を作成する
```bash
bun install
```

2.  `docker compose up --build -d` を実行してコンテナを起動する。
```bash
docker compose up --build -d
```
3.  Prisma Migrateを実行して、`schema.prisma`の内容をデータベースに反映させる。

```bash
# コンテナ内でPrisma Migrateコマンドを実行(initは任意の名前)
docker compose exec app bunx prisma migrate dev --name init
```
このコマンドは、`prisma/migrations`ディレクトリにマイグレーションファイルを生成し、データベースにテーブルを作成する。

#### 4.2. Web APIの実行

1.  `docker compose up --build` を実行する(実行済みなら無視)
```bash
docker compose up --build -d
```
2.  `curl`でAPIを叩く。

```bash
# ユーザーを作成
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Prisma User", "email": "prisma.user@example.com"}'
```
```bash
# ユーザーを取得
curl http://localhost:3000/api/users/<返ってきたID>
```

#### 4.3 データベースとRedisの動作確認

**MySQLデータベースの確認**
データベース内にデータが格納されたかを確認する．

:::note warn
本来はrootユーザーで入らず，適切な権限をユーザーを作成しないといけないが今回は割愛する
:::

```bash
docker compose exec db mysql -u root -prootpassword testdb -e "SELECT * FROM users;"
```

実行結果(idは例です)
```bash
docker compose exec db mysql -u root -prootpassword testdb -e "SELECT * FROM users;"
+--------------------------------------+-------------------------+-------------+
| id                                   | email                   | name        |
+--------------------------------------+-------------------------+-------------+
| de1ea905-7b9f-4792-9ffd-b6c5b25d6527 | prisma.user@example.com | Prisma User |
+--------------------------------------+-------------------------+-------------+
```

**Redisの動作確認**
APIがmysqlではなくredisを使用するにはdocker-compose.ymlの`- USE_USER_REPO=redis`のコメントアウトを外す．(ように設定しています)

以下のコマンドで再度コンテナを起動する
```bash
docker compose up --build -d
```

Redisが正常に動作しているかを確認するには以下のコマンドを実行：

```bash
# Redisコンテナに接続してpingコマンドを実行
docker compose exec redis redis-cli ping
```

正常に動作している場合、以下のレスポンスが返ります：
```bash
PONG
```

**Redisにデータを保存して動作確認**

環境変数を設定してRedisリポジトリを使用する場合：

```bash
# 別のターミナルでユーザーを作成（Redisに保存される）
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Redis User", "email": "redis.user@example.com"}'
```

Redisに保存されたデータを直接確認：
```bash
# Redisのすべてのキーを表示
docker compose exec redis redis-cli keys "*"
```
```bash
# 特定のキーの値を取得（上で取得したキーを指定）
docker compose exec redis redis-cli get "user:<ここにユーザーID>"
```


#### 4.4. CLIツールの実行

`cli.ts`はWeb APIと同様にUsecaseを再利用する。
このように

```src/cli.ts
import { UserUsecase } from '@/usecase/userUsecase';
import { MysqlUserRepository } from '@/infrastructure/mysql/mysqlUserRepository';
import { RedisUserRepository } from '@/infrastructure/redis/redisUserRepository';
import { disconnectPrisma } from '@/infrastructure/mysql/prismaClient';
import { disconnectRedis, getRedisClient } from '@/infrastructure/redis/redisClient';
import type { IUserRepository } from '@/domain/repository/userRepository';

async function main() {
  const command = process.argv[2];
  const name = process.argv[3];
  const email = process.argv[4];

  if (command === 'createUser') {
    if (!name || !email) {
      console.error('Usage: bun run src/cli.ts createUser <name> <email>');
      process.exit(1);
    }

    try {
      // Setup repository (same logic as in index.ts)
      const USE_REDIS_FOR_USERS = process.env.USE_USER_REPO === 'redis';
      let userRepository: IUserRepository;

      if (USE_REDIS_FOR_USERS) {
        try {
          await getRedisClient();
          userRepository = new RedisUserRepository();
          console.log('Using RedisUserRepository for CLI.');
        } catch (redisError) {
          console.error('CLI: Failed to connect to Redis, falling back to MySQL:', redisError);
          userRepository = new MysqlUserRepository();
        }
      } else {
        userRepository = new MysqlUserRepository();
        console.log('Using MySQLUserRepository for CLI.');
      }

      const userUsecase = new UserUsecase(userRepository);
      const newUser = await userUsecase.createUser({ name, email });

      console.log('✅ User created successfully:');
      console.log(`ID: ${newUser.id}`);
      console.log(`Name: ${newUser.name}`);
      console.log(`Email: ${newUser.email}`);
    } catch (error) {
      console.error('Error creating user:', error);
      process.exit(1);
    }
  } else {
    console.log('Available commands:');
    console.log('  createUser <name> <email> - Create a new user');
  }

  await disconnectPrisma();
  await disconnectRedis();
}

main().catch(console.error);
```
ビルドして実行
```bash
docker compose up --build
```

```bash
# CLIでユーザーを作成
docker compose exec app bun run src/cli.ts createUser "CLI User" "cli@example.com"
```

データが入ったかを確認
```bash
docker compose exec db mysql -u root -prootpassword testdb -e "SELECT * FROM users;"
+--------------------------------------+-------------------------+-------------+
| id                                   | email                   | name        |
+--------------------------------------+-------------------------+-------------+
| 8bff1ed3-4e49-4761-a4aa-6b7e193ea360 | cli@example.com         | CLI User    |
| de1ea905-7b9f-4792-9ffd-b6c5b25d6527 | prisma.user@example.com | Prisma User |
+--------------------------------------+-------------------------+-------------+

```
このようにcliツールとしてもデータベースに格納できた

redisの動作確認もする
docker-compose.ymlの`- USE_USER_REPO=redis`のコメントアウトを外して以下のコマンドを実行
```bash
docker compose up --build -d
```

Redisが正常に動作しているかを確認するには以下のコマンドを実行：
```bash
docker compose exec redis redis-cli ping
```
```bash
PONG
```

redisにデータを保存する
```bash
# CLIでユーザーを作成
docker compose exec app bun run src/cli.ts createUser "CLI Redis User" "cli.redis@example.com"
```

redisにデータが保存されたかを確認
```bash
docker compose exec redis redis-cli keys "user:*"
```
```bash
docker compose exec redis redis-cli get "user:<ここにユーザーID>"
```
---

Prismaを導入することで、特にMySQLとの連携において型安全で効率的な開発が可能になる。クリーンアーキテクチャでは、このPrismaを使った実装(`MysqlUserRepository`)と、全く異なる技術（Redis）を使った実装(`RedisUserRepository`)を、ビジネスロジック（Usecase）に一切変更を加えることなく差し替えられる。これが抽象に依存することの真価である。プロジェクトの要件に応じて適切なアーキテクチャとツールを選択することが、持続可能な開発の鍵となるでしょう．

---

この記事では、Bun + Hono.js + Prisma ORMを使用したクリーンアーキテクチャの実装方法を解説しました。

**クリーンアーキテクチャの主要な価値：**

🔄 **変更に強い設計**
データベースやフレームワークの変更時に、ビジネスロジックを修正する必要がありません。

🧪 **テスト容易性**
ビジネスロジックを技術的詳細から分離することで、単体テストが書きやすくなります。

🔄 **再利用性**
同じビジネスロジックを、Web API、CLI、バッチ処理などで再利用できます。

> 実際にWebAPI(index.ts)からCLI(cli.ts)に実行を変更する場合は、UsecaseやHandlerを修正しませんでしたね

また、データ保持媒体の変更時に、ビジネスロジックを修正する必要がないので、データ保持媒体の変更が容易になります。例えば，MySQLからRedisに変更する場合でも、ビジネスロジックを修正する必要がないので、データ保持媒体の変更が容易になる

> 実際にMySQLからRedisに変更する場合は、docker-compose.ymlの`- USE_USER_REPO=redis`のコメントアウトを外しただけで，Domain EntityやDomain RepositoryやUsecaseを修正しませんでしたね

このように、クリーンアーキテクチャではビジネスロジックを変更することなく，外側(インフラ層(MySQL実装やRedis実装)やUI層(Web APIやCLI))の変更のみで良いので開発者は集中してビジネスロジックの開発ができるのがポイントです！

**実装のポイント：**
- **SOLID原則**の実践により、保守性と拡張性を確保
- **依存性注入(DI)**によって、抽象に依存する設計を実現
- **層分離**により、技術的詳細とビジネスロジックを分離

初期開発コストは若干高くなりますが、長期的な保守・拡張において大きな価値があるのではないでしょうか？変化の激しいソフトウェア開発において、**「変更に強い設計」**こそが持続可能な開発の鍵となると思いますのでぜひアーキテクチャを意識した開発をしてみてください！