アーキテクチャ設計について備忘録
このドキュメントは、プロジェクトの層構造とアーキテクチャパターンについて説明します。
レイヤードアーキテクチャとは
アプリケーションを責務ごとに分離し、各層が特定の役割だけを担当する設計パターンです。
メリット
- 保守性: 各層の責務が明確で、変更の影響範囲を限定できる
- テスト容易性: 層ごとに独立してテスト可能
- 再利用性: ビジネスロジックを他のプロジェクトでも使える
- 変更容易性: フレームワーク変更時、ビジネスロジックは影響を受けない
一般的な層の分類
1. プレゼンテーション層(Presentation Layer / UI Layer)
役割: ユーザーとのやり取りを担当
含まれるもの:
- UI コンポーネント
- ページ
- フォーム
- ユーザー入力の受け取り
例:
-
src/app/- Next.js のページ -
src/features/tools/*/Tool.tsx- 各ツールの UI -
src/components/- 共通 UI コンポーネント
特徴:
- React コンポーネントが中心
- ユーザーイベント(クリック、入力等)を処理
- 見た目とユーザー体験を担当
- 他の層に依存するが、他の層から依存されない
コード例:
// src/components/layout/Header.tsx
export function SiteHeader() {
const t = useTranslations("header");
return (
<header>
<h1>{t("siteTitle")}</h1>
<nav>
<Link href="/">{t("nav.home")}</Link>
</nav>
</header>
);
}
2. アプリケーション層(Application Layer / Service Layer)
役割: ビジネスロジックの調整・ワークフローの制御
含まれるもの:
- ユースケースの実装
- 複数のドメインロジックを組み合わせた処理
- トランザクション管理
- データの取得・変換・整形
例:
-
src/lib/tools/registry.ts- ツールの管理・調整 -
src/lib/seo/metadata.ts- SEO メタデータ生成 -
src/lib/seo/jsonld.ts- JSON-LD 構造化データ生成
特徴:
- 「何を実行するか」を定義
- 複数の純粋ロジックを組み合わせる
- UI とビジネスロジックの橋渡し
- ドメイン層を呼び出す
コード例:
// src/lib/tools/registry.ts
export async function getToolCards(locale: Locale): Promise<ToolCardMeta[]> {
// 複数のツールの情報を取得・整形するワークフロー
const cards = await Promise.all(
toolDefinitions.map(async (definition) => {
const result = await loadToolStrings(definition, locale);
if (!result) return undefined;
return {
slug: definition.slug,
category: definition.category,
tags: definition.tags,
card: result.card,
};
})
);
return cards.filter((card): card is ToolCardMeta => Boolean(card));
}
3. ドメイン層(Domain Layer / Business Logic Layer)
役割: 純粋なビジネスロジック
含まれるもの:
- 計算処理
- 変換処理
- ビジネスルール
- バリデーションロジック
例:
-
src/lib/tools/engine/- BMI計算、和暦変換など -
src/features/tools/*/schema.ts- Zod によるバリデーション
特徴:
- React に依存しない純粋関数
- テストが容易
- 「どう計算するか」を定義
- 他のプロジェクトでも再利用可能
- 外部の層を知らない(依存しない)
コード例:
// src/lib/tools/engine/bmi.ts
export function calculateBmi(heightCm: number, weightKg: number): number {
const heightM = heightCm / 100;
return weightKg / (heightM * heightM);
}
export function getBmiCategory(bmi: number, locale: Locale): string {
const categories = {
ja: {
underweight: "低体重",
normal: "普通体重",
overweight: "肥満(1度)",
obese: "肥満(2度以上)",
},
en: {
underweight: "Underweight",
normal: "Normal weight",
overweight: "Overweight",
obese: "Obese",
},
};
const cat = categories[locale] || categories.en;
if (bmi < 18.5) return cat.underweight;
if (bmi < 25) return cat.normal;
if (bmi < 30) return cat.overweight;
return cat.obese;
}
→ React も Next.js も知らない。どこでも使える純粋関数。
4. インフラストラクチャ層(Infrastructure Layer)
役割: 外部システムとのやり取り
含まれるもの:
- データベースアクセス
- API 呼び出し
- ファイルシステム操作
- 外部サービス連携
- キャッシュ管理
例:
- 現状は静的サイトなので該当なし
- 将来的に API やデータベースを導入する場合はここに配置
特徴:
- 外部依存を隔離
- モックに置き換えやすい
- データの永続化を担当
- ドメイン層のインターフェースに従う
コード例(将来的な拡張例):
// src/lib/infrastructure/database.ts(未実装)
export async function saveUserPreference(userId: string, locale: Locale) {
await db.userPreferences.upsert({
where: { userId },
update: { locale },
create: { userId, locale },
});
}
5. フレームワーク層(Framework Layer / Configuration Layer)
役割: フレームワーク固有の設定・初期化
含まれるもの:
- フレームワークの設定ファイル
- ミドルウェア
- ルーティング設定
- プラグイン設定
- グローバル初期化
このプロジェクトでは:
-
src/i18n/- next-intl の設定 -
middleware.ts- Next.js ミドルウェア -
next.config.ts- Next.js 設定 -
tailwind.config.ts- Tailwind CSS 設定 -
components.json- shadcn/ui 設定
特徴:
- フレームワーク依存が強い
- アプリケーション全体の初期化
- ビジネスロジックを含まない
- フレームワークを変更すると全面的に書き換えが必要
コード例:
// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";
export const routing = defineRouting({
locales: ["ja", "en"],
defaultLocale: "en",
localePrefix: "always",
});
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing);
→ next-intl に完全に依存。Next.js でしか動かない。
このプロジェクトの層構成
層とディレクトリの対応表
| 層 | ディレクトリ | 依存関係 | 説明 |
|---|---|---|---|
| フレームワーク層 |
src/i18n/, middleware.ts, *.config.ts
|
Next.js, next-intl に依存 | フレームワーク設定、ミドルウェア |
| プレゼンテーション層 |
src/app/, src/components/, src/features/*/Tool.tsx
|
React, Next.js に依存 | UI、ページ、ユーザー入力 |
| アプリケーション層 |
src/lib/tools/registry.ts, src/lib/seo/
|
ドメイン層に依存 | ツール管理、SEO生成 |
| ドメイン層 |
src/lib/tools/engine/, src/features/*/schema.ts
|
フレームワーク非依存 | ビジネスロジック、バリデーション |
| インフラストラクチャ層 | (未実装) | - | 将来的に API やデータベースを導入する場合 |
依存関係の図
┌─────────────────────────────────────────────────────────┐
│ フレームワーク層 (src/i18n/, middleware.ts) │
│ - next-intl 設定 │
│ - Next.js ミドルウェア │
└─────────────────────────────────────────────────────────┘
↓ 依存
┌─────────────────────────────────────────────────────────┐
│ プレゼンテーション層 (src/app/, src/components/) │
│ - UI コンポーネント │
│ - ページ │
└─────────────────────────────────────────────────────────┘
↓ 依存
┌─────────────────────────────────────────────────────────┐
│ アプリケーション層 (src/lib/tools/registry.ts) │
│ - ツール管理 │
│ - SEO メタデータ生成 │
└─────────────────────────────────────────────────────────┘
↓ 依存
┌─────────────────────────────────────────────────────────┐
│ ドメイン層 (src/lib/tools/engine/) │
│ - BMI計算 │
│ - 和暦変換 │
│ - 純粋なビジネスロジック │
└─────────────────────────────────────────────────────────┘
重要な原則:
- 下の層は上の層を知らない(依存しない)
- 上の層は下の層に依存する
- ドメイン層は最も内側で、フレームワークに依存しない
なぜ src/i18n/ を独立させるのか
よくある疑問
「src/i18n/ は設定だから src/lib/ に入れるべきでは?」
答え
src/lib/ = ビジネスロジック層(ドメイン・アプリケーション)
src/i18n/ = フレームワーク設定層
この2つは性質が全く異なるため、分離すべきです。
比較表
| 観点 | src/lib/ |
src/i18n/ |
|---|---|---|
| 依存 | フレームワーク非依存 | next-intl に強く依存 |
| 再利用性 | 他のプロジェクトでも使える | Next.js + next-intl でのみ使える |
| テスト | 純粋関数なので簡単 | フレームワークのモックが必要 |
| 変更頻度 | ビジネス要件で変わる | フレームワーク更新で変わる |
| 移植性 | 高い(他のフレームワークでも使える) | 低い(Next.js 専用) |
| 責務 | 「何を計算するか」 | 「どう設定するか」 |
具体例で理解する
ドメイン層(lib/tools/engine/bmi.ts)
export function calculateBmi(heightCm: number, weightKg: number): number {
const heightM = heightCm / 100;
return weightKg / (heightM * heightM);
}
特徴:
- React も Next.js も知らない
- どこでも使える純粋関数
- Node.js、Deno、ブラウザ、どこでも動く
- テストが超簡単
// テスト
expect(calculateBmi(170, 65)).toBeCloseTo(22.49);
フレームワーク層(i18n/routing.ts)
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["ja", "en"],
defaultLocale: "en",
});
特徴:
- next-intl に完全に依存
- Next.js でしか動かない
- ビジネスロジックを含まない
- フレームワーク変更時は全面書き換え
なぜ分離するのか
- 責務の明確化: 設定とロジックを混ぜない
- テスト容易性: ビジネスロジックは純粋関数でテスト簡単
- 移植性: ドメイン層は他のフレームワークでも使える
- 変更の影響範囲: フレームワーク変更時、ビジネスロジックは影響を受けない
-
next-intl 公式推奨: 公式ドキュメントで
src/i18n/配置を推奨
クリーンアーキテクチャとの関連
クリーンアーキテクチャの原則
有名な「クリーンアーキテクチャ」(Robert C. Martin)では、依存関係を以下のように定義します:
外側(フレームワーク・UI・インフラ)
↓ 依存
中間(アプリケーション・ユースケース)
↓ 依存
内側(ドメイン・ビジネスロジック)
重要な原則
-
依存関係逆転の原則(Dependency Inversion)
- 内側の層は外側の層を知らない
- 外側の層は内側の層に依存する
- ビジネスロジックはフレームワークに依存しない
-
単一責任の原則(Single Responsibility)
- 各層は1つの責務のみを持つ
- 変更理由は1つだけ
-
開放閉鎖の原則(Open/Closed)
- 拡張に対して開いている
- 修正に対して閉じている
このプロジェクトでの実践
src/i18n/ (フレームワーク層)
↓ 依存
src/app/, src/components/ (プレゼンテーション層)
↓ 依存
src/lib/tools/registry.ts (アプリケーション層)
↓ 依存
src/lib/tools/engine/ (ドメイン層)
具体例:
engine/bmi.ts は next-intl も React も知らない。
→ だから他のプロジェクトでも使えるし、テストも簡単。
i18n/routing.ts は next-intl に依存する。
→ でもビジネスロジックは含まないので、フレームワーク変更時も影響範囲は限定的。
実装例で理解する
例1: BMI計算ツール
ドメイン層
// src/lib/tools/engine/bmi.ts
export function calculateBmi(heightCm: number, weightKg: number): number {
const heightM = heightCm / 100;
return weightKg / (heightM * heightM);
}
- 純粋関数
- フレームワーク非依存
- どこでも使える
アプリケーション層
// src/lib/tools/registry.ts
{
slug: "bmi",
category: "health",
tags: ["health", "calculator"],
load: () => import("@/features/tools/bmi/Tool"),
getStrings: async (locale) => {
const strings = await import(`@/features/tools/bmi/strings/${locale}`);
return { card: strings.default.card, ui: strings.default.ui };
},
}
- ツールの管理・調整
- 複数のリソースを組み合わせる
プレゼンテーション層
// src/features/tools/bmi/Tool.tsx
export default function BmiTool({ locale, strings }: Props) {
const [result, setResult] = useState<number | null>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
// バリデーション...
const bmi = calculateBmi(heightCm, weightKg); // ← ドメイン層を呼び出す
setResult(bmi);
};
return <form onSubmit={handleSubmit}>...</form>;
}
- UI表示
- ユーザー入力処理
- ドメイン層を呼び出す
フレームワーク層
// src/i18n/routing.ts
export const routing = defineRouting({
locales: ["ja", "en"],
defaultLocale: "en",
});
- next-intl の設定
- ビジネスロジックを含まない
例2: フレームワーク変更時の影響
シナリオ: Next.js から別のフレームワークに移行
変更が必要:
- ✗
src/i18n/- 全面書き換え(next-intl から別のライブラリへ) - ✗
src/app/- 全面書き換え(Next.js App Router から別のルーティングへ) - △
src/lib/tools/registry.ts- 一部修正(動的インポート等)
変更が不要:
- ✓
src/lib/tools/engine/- そのまま使える(純粋関数) - ✓
src/features/tools/*/schema.ts- そのまま使える(Zod)
→ ビジネスロジック(ドメイン層)はフレームワークに依存しないため、資産として保護される。
まとめ
レイヤードアーキテクチャの5層
-
フレームワーク層 - フレームワーク設定(
src/i18n/) -
プレゼンテーション層 - UI・ユーザー入力(
src/app/,src/components/) -
アプリケーション層 - ワークフロー・調整(
src/lib/tools/registry.ts) -
ドメイン層 - ビジネスロジック(
src/lib/tools/engine/) - インフラストラクチャ層 - 外部システム連携(未実装)
なぜ分離するのか
- 保守性: 各層の責務が明確で、変更の影響範囲を限定できる
- テスト容易性: 層ごとに独立してテスト可能
- 再利用性: ビジネスロジックを他のプロジェクトでも使える
- 変更容易性: フレームワーク変更時、ビジネスロジックは影響を受けない
src/i18n/ を独立させる理由
-
src/lib/= ビジネスロジック層(フレームワーク非依存) -
src/i18n/= フレームワーク設定層(next-intl に依存) - 性質が全く異なるため、分離することで責務を明確化
クリーンアーキテクチャの実践
依存の方向: 外側 → 内側
フレームワーク層 (src/i18n/)
↓
プレゼンテーション層 (src/app/, src/components/)
↓
アプリケーション層 (src/lib/tools/registry.ts)
↓
ドメイン層 (src/lib/tools/engine/)
内側の層(ドメイン層)は外側の層を知らない。
これにより、ビジネスロジックがフレームワークから独立し、長期的な保守性が向上する。
Discussion