TechHub

エンジニアの成長をサポートする技術情報サイト

← 記事一覧に戻る

レイヤードアーキテクチャとは?階層化設計の基本と実践

公開日: 2024年2月8日 著者: mogura
レイヤードアーキテクチャとは?階層化設計の基本と実践

疑問

レイヤードアーキテクチャとは何で、どのようにシステムを階層化すればよいのでしょうか?各レイヤーの役割と依存関係について一緒に学んでいきましょう。

導入

レイヤードアーキテクチャ(階層化アーキテクチャ)は、システムを機能ごとに階層に分割する最も基本的なアーキテクチャパターンです。多くのアプリケーションで採用されており、コードの整理と保守性の向上に役立ちます。

本記事では、レイヤードアーキテクチャの基本概念から、各レイヤーの役割、依存関係の管理、実践的な実装パターンまで、段階的に解説していきます。

レイヤードアーキテクチャのイメージ

解説

1. レイヤードアーキテクチャとは

レイヤードアーキテクチャは、システムを水平方向に複数のレイヤー(層)に分割する設計パターンです。各レイヤーは特定の責任を持ち、上位レイヤーは下位レイヤーに依存します。

基本的な3層アーキテクチャ

最も基本的なレイヤードアーキテクチャは、3層アーキテクチャ(3-Tier Architecture)です。

3層の構成:
1. プレゼンテーション層: ユーザーインターフェースを担当
2. ビジネスロジック層: アプリケーションのビジネスルールを担当
3. データアクセス層: データベースとの通信を担当

依存関係:
- プレゼンテーション層 → ビジネスロジック層
- ビジネスロジック層 → データアクセス層
- 各層は下位の層にのみ依存し、上位の層に依存しない

メリット:
- 責任が明確に分離される
- 各層を独立して開発・テストできる
- 変更の影響範囲が限定される
- 再利用性が向上する

2. 各レイヤーの役割

各レイヤーは明確な責任を持ち、特定の機能を担当します。責任を適切に分離することで、コードの保守性とテスタビリティが向上します。

レイヤードアーキテクチャでは、各レイヤーが明確な責任を持ちます。各レイヤーの役割を理解することで、適切な設計が可能になります。

プレゼンテーション層(Presentation Layer)

プレゼンテーション層は、ユーザーとのインターフェースを担当する最上位のレイヤーです。

主な責任:
- ユーザーからの入力を受け取る
- 入力データの検証(フォーマットチェックなど)
- ビジネスロジック層へのリクエストの転送
- ビジネスロジック層からの結果をユーザーに表示
- エラーハンドリングとユーザーフレンドリーなエラーメッセージの表示

実装例:
- Webアプリケーション: HTML、CSS、JavaScript、React、Vue.jsなど
- モバイルアプリ: iOS、AndroidのUIコンポーネント
- デスクトップアプリ: Windows Forms、WPF、Electronなど

注意点:
- ビジネスロジックを含めない
- データベースに直接アクセスしない
- 他のレイヤーの実装詳細を知らない

ビジネスロジック層(Business Layer)

ビジネスロジック層は、アプリケーションの中核となるビジネスルールとロジックを担当します。

主な責任:
- ビジネスルールの実装(例: 注文の合計金額計算、在庫チェック)
- データの検証(ビジネスルールに基づく検証)
- トランザクション管理
- データアクセス層へのアクセス制御
- 複数のデータアクセス操作の調整

実装例:
- サービスクラス(Service Classes)
- ユースケース(Use Cases)
- ドメインモデル(Domain Models)
- アプリケーションサービス(Application Services)

注意点:
- プレゼンテーション層の実装詳細を知らない
- データベースの実装詳細を知らない(データアクセス層を通じてアクセス)
- ビジネスロジックのみを含む

データアクセス層(Data Access Layer)

データアクセス層は、データの永続化と取得を担当する最下位のレイヤーです。

主な責任:
- データベースへの接続管理
- CRUD操作(Create、Read、Update、Delete)の実装
- SQLクエリの実行
- データベースエラーの処理
- データのマッピング(データベース形式 ↔ アプリケーション形式)

実装例:
- リポジトリパターン(Repository Pattern)
- DAO(Data Access Object)
- ORM(Object-Relational Mapping): Entity Framework、Hibernate、TypeORMなど
- データマッパー(Data Mapper)

注意点:
- ビジネスロジックを含めない
- データベースの実装詳細のみを扱う
- 他のレイヤーから独立して変更可能

3. 4層アーキテクチャ

4層アーキテクチャは、3層アーキテクチャにドメインモデル層を追加したより洗練された構造です。ドメイン駆動設計(DDD)の原則に基づいています。

4層アーキテクチャは、3層アーキテクチャを拡張したもので、ドメインモデル層を追加することで、より明確な責任分離を実現します。

4層の構成:
1. プレゼンテーション層: UIとユーザーインタラクション
2. アプリケーション層: ユースケースの調整とオーケストレーション
3. ドメイン層: ビジネスルールとドメインモデル
4. インフラストラクチャ層: データアクセス、外部サービス連携

各層の役割:

アプリケーション層:
- ユースケースの実装
- トランザクション管理
- ドメイン層とインフラストラクチャ層の調整
- 認証・認可の制御

ドメイン層:
- ビジネスルールの実装
- ドメインモデル(エンティティ、値オブジェクト)
- ドメインサービス
- 他の層に依存しない(最も独立した層)

インフラストラクチャ層:
- データアクセス(リポジトリの実装)
- 外部APIとの通信
- ファイルシステムへのアクセス
- メッセージキューとの連携

メリット:
- ドメインロジックが他の層から完全に分離される
- ドメイン層を独立してテストできる
- ビジネスルールの変更が他の層に影響しない
- ドメイン駆動設計との親和性が高い

4. 依存関係の方向

レイヤードアーキテクチャでは、依存関係の方向が重要です。適切な依存関係の管理により、テスタビリティと柔軟性が向上します。

レイヤードアーキテクチャでは、依存関係の方向を適切に管理することが重要です。基本的には上位層が下位層に依存しますが、依存性逆転の原則を適用することで、より柔軟な設計が可能になります。

依存性逆転の原則

依存性逆転の原則(Dependency Inversion Principle)は、SOLID原則の1つで、高レベルのモジュールが低レベルのモジュールに依存すべきではなく、両方が抽象に依存すべきという原則です。

従来の依存関係:

プレゼンテーション層
    ↓
ビジネスロジック層
    ↓
データアクセス層


依存性逆転を適用した場合:
プレゼンテーション層
    ↓
ビジネスロジック層 ← インターフェース
    ↓                    ↑
データアクセス層 ───────┘


実装方法:
1. インターフェースの定義: ビジネスロジック層でリポジトリのインターフェースを定義
2. 実装の分離: データアクセス層でインターフェースを実装
3. 依存性注入: ビジネスロジック層に実装を注入

メリット:
- ビジネスロジック層がデータアクセス層の実装に依存しない
- モックやスタブを使ったテストが容易
- データアクセス層の実装を変更しても、ビジネスロジック層に影響しない
- 複数の実装(例: データベース、メモリ、ファイル)を切り替え可能

5. 実践的な実装例

Node.jsとExpressを使用したレイヤードアーキテクチャの実装例を示します。各レイヤーの責任を明確に分離した実装方法を説明します。

実践的な実装例として、Node.jsとExpressを使用したレイヤードアーキテクチャを示します。各レイヤーがどのように実装されるかを理解することで、実際のプロジェクトに適用できます。

Node.js + Expressの例

Node.jsとExpressを使用したユーザー管理システムの例です。

プロジェクト構造:

src/
├── controllers/      # プレゼンテーション層
│   └── userController.js
├── services/          # ビジネスロジック層
│   └── userService.js
├── repositories/      # データアクセス層
│   └── userRepository.js
├── models/            # データモデル
│   └── user.js
└── routes/            # ルーティング
    └── userRoutes.js


各レイヤーの実装:

1. データアクセス層(Repository):
- データベース操作を担当
- SQLクエリやORMの操作を実装

2. ビジネスロジック層(Service):
- ビジネスルールを実装
- データ検証やビジネスロジックの実行
- Repositoryを呼び出してデータを取得・保存

3. プレゼンテーション層(Controller):
- HTTPリクエストの処理
- リクエストデータの検証(フォーマット)
- Serviceを呼び出してビジネスロジックを実行
- HTTPレスポンスの生成

データアクセス層(Repository)

データアクセス層の実装例です。データベース操作のみを担当します。

// データアクセス層: repositories/userRepository.js
class UserRepository {
  async findById(id) {
    // データベースからユーザーを取得
    return await db.query('SELECT * FROM users WHERE id = ?', [id]);
  }

  async create(userData) {
    // データベースにユーザーを作成
    return await db.query('INSERT INTO users ...', [userData]);
  }

  async update(id, userData) {
    // データベースのユーザーを更新
    return await db.query('UPDATE users SET ... WHERE id = ?', [userData, id]);
  }
}

module.exports = UserRepository;

ビジネスロジック層(Service)

ビジネスロジック層の実装例です。ビジネスルールと検証を担当します。

// ビジネスロジック層: services/userService.js
const UserRepository = require('../repositories/userRepository');

class UserService {
  constructor() {
    this.userRepository = new UserRepository();
  }

  async getUserById(id) {
    if (!id) {
      throw new Error('User ID is required');
    }
    
    const user = await this.userRepository.findById(id);
    
    if (!user) {
      throw new Error('User not found');
    }
    
    return user;
  }

  async createUser(userData) {
    // ビジネスルール: メールアドレスの重複チェック
    const existingUser = await this.userRepository.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('Email already exists');
    }
    
    // ビジネスルール: パスワードの検証
    if (userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    
    return await this.userRepository.create(userData);
  }
}

module.exports = UserService;

プレゼンテーション層(Controller)

プレゼンテーション層の実装例です。HTTPリクエストの処理とレスポンスの生成を担当します。

// プレゼンテーション層: controllers/userController.js
const UserService = require('../services/userService');

class UserController {
  constructor() {
    this.userService = new UserService();
  }

  async getUser(req, res) {
    try {
      const { id } = req.params;
      
      // フォーマット検証
      if (!id || isNaN(id)) {
        return res.status(400).json({ error: 'Invalid user ID' });
      }
      
      const user = await this.userService.getUserById(parseInt(id));
      res.json(user);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async createUser(req, res) {
    try {
      const userData = req.body;
      
      // フォーマット検証
      if (!userData.email || !userData.password) {
        return res.status(400).json({ error: 'Email and password are required' });
      }
      
      const user = await this.userService.createUser(userData);
      res.status(201).json(user);
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
}

module.exports = UserController;

ルーティング

ルーティングの実装例です。URLとコントローラーのメソッドをマッピングします。

// ルーティング: routes/userRoutes.js
const express = require('express');
const UserController = require('../controllers/userController');

const router = express.Router();
const userController = new UserController();

router.get('/users/:id', (req, res) => userController.getUser(req, res));
router.post('/users', (req, res) => userController.createUser(req, res));

module.exports = router;

6. レイヤー間の通信

レイヤー間の通信方法は、システムの柔軟性とテスタビリティに大きく影響します。適切な通信方法を選択することが重要です。

レイヤー間の通信には、直接的な依存と依存性注入(DI)の2つのアプローチがあります。それぞれ異なる特徴と用途があります。

直接的な依存

直接的な依存は、レイヤーが直接他のレイヤーの実装に依存する方法です。

特徴:
- シンプルで理解しやすい
- 実装が簡単
- 結合度が高い
- テストが困難(モックが難しい)
- 変更の影響範囲が広い

:

class UserService {
  constructor() {
    this.userRepository = new UserRepository(); // 直接インスタンス化
  }
}


問題点:
- UserServiceがUserRepositoryの実装に直接依存している
- テスト時に実際のデータベースが必要
- 別の実装(例: メモリリポジトリ)に切り替えにくい

依存性注入(DI)

依存性注入(Dependency Injection)は、依存関係を外部から注入する方法です。

特徴:
- 結合度が低い
- テストが容易(モックを注入可能)
- 柔軟性が高い(実装を切り替え可能)
- 実装がやや複雑

実装方法:

1. コンストラクタインジェクション:

class UserService {
     constructor(userRepository) {
       this.userRepository = userRepository; // 外部から注入
     }
   }
   
   const userRepository = new UserRepository();
   const userService = new UserService(userRepository);


2. セッターインジェクション:
class UserService {
     setUserRepository(userRepository) {
       this.userRepository = userRepository;
     }
   }


3. DIコンテナの使用:
- InversifyJS、AwilixなどのDIコンテナを使用
- 依存関係を自動的に解決

メリット:
- テスト時にモックリポジトリを注入可能
- 実装を簡単に切り替え可能(例: データベース ↔ メモリ)
- 依存関係が明確になる
- 単一責任の原則に従いやすい

依存性注入の実装例

依存性注入を使った実装例です。テスト時にモックを注入できます。

// 依存性注入の例
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async getUserById(id) {
    return await this.userRepository.findById(id);
  }
}

// 本番環境: データベースリポジトリを注入
const dbRepository = new UserRepository();
const userService = new UserService(dbRepository);

// テスト環境: モックリポジトリを注入
const mockRepository = {
  findById: async (id) => ({ id, name: 'Test User' })
};
const testUserService = new UserService(mockRepository);

7. レイヤーの責任分離

各レイヤーが明確な責任を持つことで、コードの保守性と理解しやすさが向上します。責任の境界を明確にすることが重要です。

レイヤードアーキテクチャの成功の鍵は、各レイヤーの責任を明確に分離することです。各レイヤーが何を担当し、何を担当すべきでないかを理解することが重要です。

各レイヤーが持つべき責任

プレゼンテーション層:
- ✅ HTTPリクエストの受信とレスポンスの生成
- ✅ 入力データのフォーマット検証
- ✅ エラーメッセージのユーザーフレンドリーな表示
- ❌ ビジネスロジックの実装
- ❌ データベースへの直接アクセス

ビジネスロジック層:
- ✅ ビジネスルールの実装
- ✅ データのビジネス検証(例: 在庫チェック、価格計算)
- ✅ トランザクション管理
- ✅ 複数のリポジトリ操作の調整
- ❌ HTTPリクエストの直接処理
- ❌ SQLクエリの直接実行

データアクセス層:
- ✅ データベース操作の実装
- ✅ SQLクエリの実行
- ✅ データのマッピング
- ✅ データベースエラーの処理
- ❌ ビジネスロジックの実装
- ❌ データのビジネス検証

責任の境界を守る重要性:
- 各レイヤーが自分の責任のみを担当することで、変更の影響範囲が限定される
- レイヤー間の結合度が低くなり、独立してテスト・変更可能
- コードの理解が容易になり、新しいメンバーのオンボーディングが簡単

8. よくある間違いとベストプラクティス

レイヤードアーキテクチャを実装する際によくある間違いと、それを避けるためのベストプラクティスを説明します。

レイヤードアーキテクチャを実装する際には、いくつかのよくある間違いがあります。これらの間違いを避けることで、より保守しやすいシステムを構築できます。

❌ 悪い例:レイヤーの責任が混在

問題のあるコード:

// Controllerでビジネスロジックとデータアクセスが混在
class UserController {
  async createUser(req, res) {
    const { email, password } = req.body;
    
    // ❌ ビジネスロジックがControllerにある
    if (password.length < 8) {
      return res.status(400).json({ error: 'Password too short' });
    }
    
    // ❌ データベースに直接アクセス
    const existingUser = await db.query('SELECT * FROM users WHERE email = ?', [email]);
    if (existingUser) {
      return res.status(400).json({ error: 'Email exists' });
    }
    
    // ❌ SQLクエリがControllerにある
    await db.query('INSERT INTO users ...', [email, password]);
    
    res.json({ success: true });
  }
}


問題点:
- Controllerにビジネスロジックが含まれている
- Controllerがデータベースに直接アクセスしている
- テストが困難(HTTPリクエストが必要)
- ビジネスロジックの再利用ができない
- 変更の影響範囲が広い

✅ 良い例:責任が明確に分離

適切に分離されたコード:

// Controller: HTTPリクエストの処理のみ
class UserController {
  constructor(userService) {
    this.userService = userService;
  }

  async createUser(req, res) {
    try {
      const userData = req.body;
      
      // ✅ フォーマット検証のみ
      if (!userData.email || !userData.password) {
        return res.status(400).json({ error: 'Email and password required' });
      }
      
      // ✅ Serviceに委譲
      const user = await this.userService.createUser(userData);
      res.status(201).json(user);
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
}

// Service: ビジネスロジック
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async createUser(userData) {
    // ✅ ビジネスルールの実装
    if (userData.password.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    
    const existingUser = await this.userRepository.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('Email already exists');
    }
    
    return await this.userRepository.create(userData);
  }
}

// Repository: データアクセスのみ
class UserRepository {
  async findByEmail(email) {
    return await db.query('SELECT * FROM users WHERE email = ?', [email]);
  }

  async create(userData) {
    return await db.query('INSERT INTO users ...', [userData]);
  }
}


メリット:
- 各レイヤーの責任が明確
- テストが容易(各層を独立してテスト可能)
- ビジネスロジックの再利用が可能
- 変更の影響範囲が限定される
- コードの理解が容易

9. テストの容易性

レイヤードアーキテクチャは、各レイヤーを独立してテストできるため、テスタビリティが高いです。適切な設計により、単体テスト、統合テスト、E2Eテストを効率的に実装できます。

レイヤードアーキテクチャの大きなメリットの1つは、各レイヤーを独立してテストできることです。依存性注入を活用することで、モックやスタブを使った効率的なテストが可能になります。

テストの種類:

1. 単体テスト(Unit Tests):
- 各レイヤーを独立してテスト
- モックやスタブを使用して依存関係を排除
- 高速で実行可能

2. 統合テスト(Integration Tests):
- 複数のレイヤーを組み合わせてテスト
- 実際のデータベースを使用(テスト用)
- レイヤー間の連携を検証

3. E2Eテスト(End-to-End Tests):
- システム全体を通したテスト
- 実際の環境に近い状態でテスト
- ユーザーシナリオを検証

テストの例:

Service層の単体テスト:
- Repositoryをモック化
- ビジネスロジックのみをテスト
- データベース不要で高速

Repository層の統合テスト:
- 実際のデータベース(テスト用)を使用
- SQLクエリの動作を検証
- データの永続化を確認

Controller層の単体テスト:
- Serviceをモック化
- HTTPリクエスト/レスポンスの処理をテスト
- エラーハンドリングを検証

Service層の単体テスト

Service層の単体テスト例です。Repositoryをモック化して、ビジネスロジックのみをテストします。

// Service層の単体テスト例
const UserService = require('../services/userService');

describe('UserService', () => {
  let userService;
  let mockRepository;

  beforeEach(() => {
    // Repositoryをモック化
    mockRepository = {
      findByEmail: jest.fn(),
      create: jest.fn()
    };
    userService = new UserService(mockRepository);
  });

  it('should create user with valid data', async () => {
    mockRepository.findByEmail.mockResolvedValue(null);
    mockRepository.create.mockResolvedValue({ id: 1, email: 'test@example.com' });

    const userData = { email: 'test@example.com', password: 'password123' };
    const user = await userService.createUser(userData);

    expect(mockRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
    expect(mockRepository.create).toHaveBeenCalledWith(userData);
    expect(user.id).toBe(1);
  });

  it('should throw error if email already exists', async () => {
    mockRepository.findByEmail.mockResolvedValue({ id: 1, email: 'test@example.com' });

    const userData = { email: 'test@example.com', password: 'password123' };
    
    await expect(userService.createUser(userData)).rejects.toThrow('Email already exists');
  });

  it('should throw error if password is too short', async () => {
    const userData = { email: 'test@example.com', password: 'short' };
    
    await expect(userService.createUser(userData)).rejects.toThrow('Password must be at least 8 characters');
  });
});

10. レイヤードアーキテクチャのメリット・デメリット

レイヤードアーキテクチャには多くのメリットがありますが、デメリットも存在します。プロジェクトの要件に応じて適切に判断することが重要です。

レイヤードアーキテクチャを採用する際は、メリットとデメリットを理解し、プロジェクトの要件に適しているかを判断することが重要です。

メリット

1. 責任の明確化:
- 各レイヤーが明確な責任を持つため、コードの理解が容易
- 新しいメンバーのオンボーディングが簡単
- コードレビューがしやすい

2. 保守性の向上:
- 変更の影響範囲が限定される
- バグの特定が容易
- リファクタリングが安全

3. テスタビリティ:
- 各レイヤーを独立してテスト可能
- モックやスタブを使った効率的なテスト
- テストの実行速度が速い

4. 再利用性:
- ビジネスロジック層を複数のプレゼンテーション層から利用可能
- データアクセス層を複数のサービスから利用可能
- コードの重複を削減

5. チーム開発:
- 異なるチームが異なるレイヤーを担当可能
- 並行開発が容易
- コンフリクトが発生しにくい

6. 技術スタックの変更:
- データベースを変更しても、ビジネスロジック層に影響しない
- UIフレームワークを変更しても、ビジネスロジック層に影響しない
- 段階的な移行が可能

デメリット

1. オーバーエンジニアリングのリスク:
- 小規模なプロジェクトでは過剰な構造化になる可能性
- シンプルなCRUDアプリケーションには不要な場合がある
- 開発の初期コストが高い

2. パフォーマンスのオーバーヘッド:
- レイヤー間の呼び出しによるオーバーヘッド
- 小さな操作でも複数のレイヤーを通過する必要がある
- 最適化が必要な場合がある

3. 学習コスト:
- チームメンバーがレイヤードアーキテクチャを理解する必要がある
- 適切な設計判断が難しい場合がある
- 経験が必要

4. 複雑性の増加:
- ファイル数が増加する
- ディレクトリ構造が複雑になる
- 小さな変更でも複数のファイルを変更する必要がある場合がある

5. アンチパターンのリスク:
- レイヤーを通過するだけの「アンチレイヤー」が発生する可能性
- 責任が不明確になる場合がある
- 適切な設計が重要

6. スケーラビリティの制限:
- 単一のアプリケーション内でのみ有効
- マイクロサービスアーキテクチャほどスケーラブルではない
- 大規模システムでは追加のパターンが必要

11. 実践的なアーキテクチャ例

実際の電子商取引システムを例に、レイヤードアーキテクチャの実装方法を説明します。

実践的な例として、電子商取引システムでのレイヤードアーキテクチャの適用方法を説明します。

電子商取引システム

システムの要件:
- ユーザー管理(登録、ログイン、プロフィール管理)
- 商品管理(商品一覧、検索、詳細表示)
- 注文処理(カート、注文作成、注文履歴)
- 決済処理(支払い、請求書生成)

レイヤー構造:

┌─────────────────────────────────────┐
│  プレゼンテーション層                │
│  - UserController                   │
│  - ProductController                │
│  - OrderController                  │
│  - PaymentController                │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│  ビジネスロジック層                 │
│  - UserService                      │
│  - ProductService                   │
│  - OrderService                     │
│  - PaymentService                   │
│  - InventoryService                 │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│  データアクセス層                   │
│  - UserRepository                   │
│  - ProductRepository                │
│  - OrderRepository                  │
│  - PaymentRepository                │
│  - InventoryRepository              │
└─────────────────────────────────────┘


注文処理のフロー:

1. Controller: HTTPリクエストを受信
- 注文データのフォーマット検証
- Serviceに処理を委譲

2. Service: ビジネスロジックの実行
- 在庫の確認(InventoryService)
- 価格の計算
- 注文の作成(OrderRepository)
- 在庫の更新(InventoryRepository)
- 決済処理(PaymentService)

3. Repository: データの永続化
- 注文データをデータベースに保存
- 在庫データを更新

メリット:
- 各機能が独立して開発可能
- ビジネスロジックの再利用が可能
- テストが容易
- 変更の影響範囲が限定される

プロジェクト構造

電子商取引システムのプロジェクト構造の例です。

// プロジェクト構造の例
src/
├── controllers/
│   ├── userController.js
│   ├── productController.js
│   ├── orderController.js
│   └── paymentController.js
├── services/
│   ├── userService.js
│   ├── productService.js
│   ├── orderService.js
│   ├── paymentService.js
│   └── inventoryService.js
├── repositories/
│   ├── userRepository.js
│   ├── productRepository.js
│   ├── orderRepository.js
│   ├── paymentRepository.js
│   └── inventoryRepository.js
├── models/
│   ├── user.js
│   ├── product.js
│   ├── order.js
│   └── payment.js
└── routes/
    ├── userRoutes.js
    ├── productRoutes.js
    ├── orderRoutes.js
    └── paymentRoutes.js

12. ベストプラクティス

レイヤードアーキテクチャを効果的に実装するためのベストプラクティスをまとめます。

レイヤードアーキテクチャを成功させるためには、以下のベストプラクティスに従うことが重要です。

1. 明確な責任分離:
- 各レイヤーが明確な責任を持つ
- レイヤー間の責任が重複しない
- 各レイヤーが自分の責任のみを担当

2. 依存関係の方向:
- 上位層が下位層に依存する(一方向)
- 依存性逆転の原則を適用
- インターフェースを通じて依存

3. 依存性注入の使用:
- コンストラクタインジェクションを推奨
- DIコンテナの活用を検討
- テスト容易性を向上

4. 適切な粒度:
- レイヤーは小さすぎず大きすぎず
- 1つのレイヤーが1つの責任を持つ
- 過剰な分割を避ける

5. インターフェースの使用:
- リポジトリはインターフェースで定義
- サービスもインターフェースで定義(必要に応じて)
- 実装の切り替えを容易に

6. エラーハンドリング:
- 各レイヤーで適切なエラーハンドリング
- エラーの種類に応じた処理
- ユーザーフレンドリーなエラーメッセージ

7. バリデーション:
- フォーマット検証はプレゼンテーション層
- ビジネスルール検証はビジネスロジック層
- 重複を避ける

8. テスト戦略:
- 各レイヤーを独立してテスト
- モックとスタブを活用
- 統合テストでレイヤー間の連携を検証

9. ドキュメント:
- 各レイヤーの責任を文書化
- 依存関係を図示
- APIドキュメントを整備

10. 段階的な導入:
- 既存のコードに段階的に適用
- 新機能からレイヤードアーキテクチャを適用
- リファクタリングを継続的に実施

まとめ

レイヤードアーキテクチャは、システムを階層に分割することで、コードの整理と保守性を向上させる基本的なアーキテクチャパターンです。各レイヤーの責任を明確にし、依存関係を適切に管理することが重要です。

依存性逆転の原則を適用し、インターフェースを通じて依存することで、テスタビリティと柔軟性を向上できます。実践的なプロジェクトでレイヤードアーキテクチャを適用し、段階的に改善することで、より保守しやすいシステムを構築できます。

ヘキサゴナルアーキテクチャとは?ポート&アダプターパターンの実践 イベント駆動アーキテクチャとは?非同期処理と疎結合の実現