ブログ記事

Clean Architecture完全ガイド2025 - 実装から設計原則まで徹底解説

Clean Architectureの基本原則から実装方法まで詳しく解説。依存性逆転の原則、各層の責務、実践的な実装例を通じて、保守性の高いアプリケーション設計を学びます。

16分で読めます
R
Rina
Daily Hack 編集長
プログラミング
Clean Architecture 設計パターン ソフトウェア設計 DDD アーキテクチャ
Clean Architecture完全ガイド2025 - 実装から設計原則まで徹底解説のヒーロー画像

はじめに

現代のソフトウェア開発において、Clean Architectureは保守性、テスタビリティ、拡張性を実現するための重要な設計パターンです。Uncle Bob(Robert C. Martin)によって提唱されたこのアーキテクチャは、ビジネスロジックを外部の詳細から独立させることで、変更に強い設計を実現します。

本記事では、Clean Architecture の基本原則から実践的な実装方法まで、2025 年の最新トレンドを踏まえて徹底的に解説します。

この記事で学べること

  • Clean Architecture の基本原則と設計思想
  • 各層の責務と実装パターン
  • 依存性逆転の原則(DIP)の実践的な適用
  • TypeScriptを使った具体的な実装例
  • テスタビリティの向上とユニットテストの実装
  • 実プロジェクトでの適用事例とベストプラクティス

Clean Architectureとは

Clean Architecture は、ソフトウェアシステムを同心円状の層に分離し、内側の層が外側の層に依存しないように設計するアーキテクチャパターンです。この設計により、ビジネスロジックがフレームワークやデータベース、UI などの詳細から独立し、変更に強いシステムを構築できます。

Clean Architectureの層構造

チャートを読み込み中...

基本原則

Clean Architecture は以下の 4 つの重要な原則に基づいています:

Clean Architectureの基本原則
原則 説明 効果
フレームワーク独立性 ビジネスロジックが特定のフレームワークに依存しない フレームワークの変更が容易
テスタビリティ ビジネスルールが外部要素なしでテスト可能 ユニットテストの実装が簡単
UI独立性 UIの変更がシステムの他の部分に影響しない UI技術の変更が容易
データベース独立性 ビジネスルールがデータベースに依存しない データベースの変更が容易

各層の詳細と責務

1. Entities(エンティティ層)

エンティティ層は最も内側の層で、ビジネスの核となるルールとデータを含みます。この層は外部の変更から完全に独立している必要があります。

// domain/entities/User.ts
export class User {
  constructor(
    private readonly id: string,
    private readonly email: string,
    private readonly name: string,
    private readonly createdAt: Date
  ) {}

  // ビジネスルール:メールアドレスの検証
  static validateEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  // ビジネスルール:パスワードの強度チェック
  static validatePasswordStrength(password: string): {
    isValid: boolean;
    errors: string[];
  } {
    const errors: string[] = [];
    
    if (password.length < 8) {
      errors.push('パスワードは8文字以上である必要があります');
    }
    if (!/[A-Z]/.test(password)) {
      errors.push('大文字を含む必要があります');
    }
    if (!/[a-z]/.test(password)) {
      errors.push('小文字を含む必要があります');
    }
    if (!/[0-9]/.test(password)) {
      errors.push('数字を含む必要があります');
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }

  // ゲッターメソッド
  getId(): string {
    return this.id;
  }

  getEmail(): string {
    return this.email;
  }

  getName(): string {
    return this.name;
  }
}

2. Use Cases(ユースケース層)

ユースケース層は、アプリケーション固有のビジネスルールを含みます。エンティティを操作し、データの流れを調整します。

// application/usecases/CreateUserUseCase.ts
import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { IEmailService } from '../../domain/services/IEmailService';

export interface CreateUserInput {
  email: string;
  name: string;
  password: string;
}

export interface CreateUserOutput {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

export class CreateUserUseCase {
  constructor(
    private readonly userRepository: IUserRepository,
    private readonly emailService: IEmailService
  ) {}

  async execute(input: CreateUserInput): Promise<CreateUserOutput> {
    // 入力検証
    if (!User.validateEmail(input.email)) {
      throw new Error('無効なメールアドレスです');
    }

    const passwordValidation = User.validatePasswordStrength(input.password);
    if (!passwordValidation.isValid) {
      throw new Error(
        `パスワードが要件を満たしていません: ${passwordValidation.errors.join(', ')}`
      );
    }

    // 重複チェック
    const existingUser = await this.userRepository.findByEmail(input.email);
    if (existingUser) {
      throw new Error('このメールアドレスは既に使用されています');
    }

    // ユーザー作成
    const user = new User(
      this.generateId(),
      input.email,
      input.name,
      new Date()
    );

    // 保存
    await this.userRepository.save(user);

    // ウェルカムメール送信
    await this.emailService.sendWelcomeEmail(user.getEmail(), user.getName());

    // 出力の作成
    return {
      id: user.getId(),
      email: user.getEmail(),
      name: user.getName(),
      createdAt: new Date()
    };
  }

  private generateId(): string {
    return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

3. Interface Adapters(インターフェースアダプター層)

この層は、ユースケースと外部世界をつなぐアダプターの役割を果たします。コントローラー、プレゼンター、ゲートウェイなどが含まれます。

// 従来の密結合な実装
class UserController {
  async createUser(req: Request, res: Response) {
    const { email, name, password } = req.body;
    
    // 直接データベース操作
    const user = await db.users.create({
      email,
      name,
      password: await bcrypt.hash(password, 10)
    });
    
    // 直接メール送信
    await sendEmail({
      to: email,
      subject: 'Welcome!',
      body: `Hello ${name}`
    });
    
    res.json(user);
  }
}
// Clean Architectureに基づく実装
class UserController {
  constructor(
    private readonly createUserUseCase: CreateUserUseCase
  ) {}

  async createUser(req: Request, res: Response) {
    try {
      const input: CreateUserInput = {
        email: req.body.email,
        name: req.body.name,
        password: req.body.password
      };
      
      const output = await this.createUserUseCase.execute(input);
      
      res.status(201).json({
        success: true,
        data: output
      });
    } catch (error) {
      res.status(400).json({
        success: false,
        error: error.message
      });
    }
  }
}
従来の実装
// 従来の密結合な実装
class UserController {
  async createUser(req: Request, res: Response) {
    const { email, name, password } = req.body;
    
    // 直接データベース操作
    const user = await db.users.create({
      email,
      name,
      password: await bcrypt.hash(password, 10)
    });
    
    // 直接メール送信
    await sendEmail({
      to: email,
      subject: 'Welcome!',
      body: `Hello ${name}`
    });
    
    res.json(user);
  }
}
Clean Architectureの実装
// Clean Architectureに基づく実装
class UserController {
  constructor(
    private readonly createUserUseCase: CreateUserUseCase
  ) {}

  async createUser(req: Request, res: Response) {
    try {
      const input: CreateUserInput = {
        email: req.body.email,
        name: req.body.name,
        password: req.body.password
      };
      
      const output = await this.createUserUseCase.execute(input);
      
      res.status(201).json({
        success: true,
        data: output
      });
    } catch (error) {
      res.status(400).json({
        success: false,
        error: error.message
      });
    }
  }
}

4. Frameworks & Drivers(フレームワーク&ドライバー層)

最外層には、具体的な実装技術が配置されます。データベース、Web フレームワーク、外部サービスとの連携などです。

// infrastructure/repositories/UserRepository.ts
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { User } from '../../domain/entities/User';
import { PrismaClient } from '@prisma/client';

export class UserRepository implements IUserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async save(user: User): Promise<void> {
    await this.prisma.user.create({
      data: {
        id: user.getId(),
        email: user.getEmail(),
        name: user.getName(),
        createdAt: new Date()
      }
    });
  }

  async findByEmail(email: string): Promise<User | null> {
    const userData = await this.prisma.user.findUnique({
      where: { email }
    });

    if (!userData) {
      return null;
    }

    return new User(
      userData.id,
      userData.email,
      userData.name,
      userData.createdAt
    );
  }

  async findById(id: string): Promise<User | null> {
    const userData = await this.prisma.user.findUnique({
      where: { id }
    });

    if (!userData) {
      return null;
    }

    return new User(
      userData.id,
      userData.email,
      userData.name,
      userData.createdAt
    );
  }
}

依存性逆転の原則(DIP)の実装

Clean Architecture の核心は、依存性逆転の原則です。内側の層は外側の層に依存せず、代わりにインターフェースを通じて連携します。

依存性逆転の原則

チャートを読み込み中...

インターフェースの定義

// domain/repositories/IUserRepository.ts
import { User } from '../entities/User';

export interface IUserRepository {
  save(user: User): Promise<void>;
  findByEmail(email: string): Promise<User | null>;
  findById(id: string): Promise<User | null>;
  update(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// domain/services/IEmailService.ts
export interface IEmailService {
  sendWelcomeEmail(email: string, name: string): Promise<void>;
  sendPasswordResetEmail(email: string, token: string): Promise<void>;
  sendAccountDeletionConfirmation(email: string): Promise<void>;
}

テスタビリティの向上

Clean Architecture の大きな利点の 1 つは、高いテスタビリティです。各層が独立しているため、モックを使用したユニットテストが容易に実装できます。

// tests/usecases/CreateUserUseCase.test.ts
import { CreateUserUseCase } from '../../application/usecases/CreateUserUseCase';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { IEmailService } from '../../domain/services/IEmailService';

describe('CreateUserUseCase', () => {
  let useCase: CreateUserUseCase;
  let mockUserRepository: jest.Mocked<IUserRepository>;
  let mockEmailService: jest.Mocked<IEmailService>;

  beforeEach(() => {
    // モックの作成
    mockUserRepository = {
      save: jest.fn(),
      findByEmail: jest.fn(),
      findById: jest.fn(),
      update: jest.fn(),
      delete: jest.fn()
    };

    mockEmailService = {
      sendWelcomeEmail: jest.fn(),
      sendPasswordResetEmail: jest.fn(),
      sendAccountDeletionConfirmation: jest.fn()
    };

    useCase = new CreateUserUseCase(mockUserRepository, mockEmailService);
  });

  it('正常にユーザーを作成できる', async () => {
    // Arrange
    const input = {
      email: 'test@example.com',
      name: 'Test User',
      password: 'StrongPass123'
    };
    mockUserRepository.findByEmail.mockResolvedValue(null);

    // Act
    const result = await useCase.execute(input);

    // Assert
    expect(result.email).toBe(input.email);
    expect(result.name).toBe(input.name);
    expect(mockUserRepository.save).toHaveBeenCalledTimes(1);
    expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
      input.email,
      input.name
    );
  });

  it('既存のメールアドレスの場合はエラーを投げる', async () => {
    // Arrange
    const input = {
      email: 'existing@example.com',
      name: 'Test User',
      password: 'StrongPass123'
    };
    mockUserRepository.findByEmail.mockResolvedValue({} as any);

    // Act & Assert
    await expect(useCase.execute(input)).rejects.toThrow(
      'このメールアドレスは既に使用されています'
    );
  });
});

実践的な実装例:ECサイトの注文処理

より複雑な例として、EC サイトの注文処理システムを Clean Architecture で実装してみましょう。

エンティティの定義

// domain/entities/Order.ts
export class Order {
  private items: OrderItem[] = [];
  private status: OrderStatus = OrderStatus.PENDING;

  constructor(
    private readonly id: string,
    private readonly customerId: string,
    private readonly createdAt: Date
  ) {}

  addItem(productId: string, quantity: number, price: number): void {
    const existingItem = this.items.find(item => item.productId === productId);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push(new OrderItem(productId, quantity, price));
    }
  }

  getTotalAmount(): number {
    return this.items.reduce((total, item) => total + item.getSubtotal(), 0);
  }

  canBeCancelled(): boolean {
    return this.status === OrderStatus.PENDING || 
           this.status === OrderStatus.CONFIRMED;
  }

  cancel(): void {
    if (!this.canBeCancelled()) {
      throw new Error('この注文はキャンセルできません');
    }
    this.status = OrderStatus.CANCELLED;
  }

  // その他のビジネスロジック...
}

// domain/entities/OrderItem.ts
export class OrderItem {
  constructor(
    public readonly productId: string,
    public quantity: number,
    public readonly price: number
  ) {}

  getSubtotal(): number {
    return this.quantity * this.price;
  }
}

// domain/entities/OrderStatus.ts
export enum OrderStatus {
  PENDING = 'PENDING',
  CONFIRMED = 'CONFIRMED',
  PROCESSING = 'PROCESSING',
  SHIPPED = 'SHIPPED',
  DELIVERED = 'DELIVERED',
  CANCELLED = 'CANCELLED'
}

ユースケースの実装

// application/usecases/CreateOrderUseCase.ts
export class CreateOrderUseCase {
  constructor(
    private readonly orderRepository: IOrderRepository,
    private readonly productRepository: IProductRepository,
    private readonly inventoryService: IInventoryService,
    private readonly paymentService: IPaymentService,
    private readonly notificationService: INotificationService
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    // 1. 商品の存在確認と在庫チェック
    const products = await this.validateAndGetProducts(input.items);
    
    // 2. 注文の作成
    const order = new Order(
      this.generateOrderId(),
      input.customerId,
      new Date()
    );

    // 3. 注文アイテムの追加
    for (const item of input.items) {
      const product = products.find(p => p.id === item.productId)!;
      order.addItem(item.productId, item.quantity, product.price);
    }

    // 4. 在庫の予約
    await this.inventoryService.reserveItems(
      input.items.map(item => ({
        productId: item.productId,
        quantity: item.quantity
      }))
    );

    try {
      // 5. 支払い処理
      const paymentResult = await this.paymentService.processPayment({
        amount: order.getTotalAmount(),
        customerId: input.customerId,
        paymentMethodId: input.paymentMethodId
      });

      if (!paymentResult.success) {
        throw new Error('支払い処理に失敗しました');
      }

      // 6. 注文の保存
      await this.orderRepository.save(order);

      // 7. 通知送信
      await this.notificationService.sendOrderConfirmation(
        input.customerId,
        order.getId()
      );

      return {
        orderId: order.getId(),
        totalAmount: order.getTotalAmount(),
        status: 'CONFIRMED'
      };
    } catch (error) {
      // エラー時は在庫予約を解除
      await this.inventoryService.releaseReservation(
        input.items.map(item => ({
          productId: item.productId,
          quantity: item.quantity
        }))
      );
      throw error;
    }
  }

  private async validateAndGetProducts(items: OrderItemInput[]): Promise<Product[]> {
    const products: Product[] = [];
    
    for (const item of items) {
      const product = await this.productRepository.findById(item.productId);
      if (!product) {
        throw new Error(`商品が見つかりません: ${item.productId}`);
      }
      
      const stock = await this.inventoryService.getAvailableStock(item.productId);
      if (stock < item.quantity) {
        throw new Error(`在庫が不足しています: ${product.name}`);
      }
      
      products.push(product);
    }
    
    return products;
  }

  private generateOrderId(): string {
    return `ORD_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

プロジェクト構造のベストプラクティス

Clean Architecture を実装する際の推奨プロジェクト構造を紹介します。

src/
├── domain/                 # ビジネスロジック層
│   ├── entities/          # エンティティ
│   │   ├── User.ts
│   │   ├── Order.ts
│   │   └── Product.ts
│   ├── repositories/      # リポジトリインターフェース
│   │   ├── IUserRepository.ts
│   │   └── IOrderRepository.ts
│   ├── services/          # ドメインサービスインターフェース
│   │   ├── IEmailService.ts
│   │   └── IPaymentService.ts
│   └── value-objects/     # 値オブジェクト
│       ├── Email.ts
│       └── Money.ts
├── application/           # アプリケーション層
│   ├── usecases/         # ユースケース
│   │   ├── CreateUserUseCase.ts
│   │   └── CreateOrderUseCase.ts
│   └── dto/              # データ転送オブジェクト
│       ├── CreateUserDto.ts
│       └── CreateOrderDto.ts
├── infrastructure/        # インフラストラクチャ層
│   ├── repositories/     # リポジトリ実装
│   │   ├── UserRepository.ts
│   │   └── OrderRepository.ts
│   ├── services/         # サービス実装
│   │   ├── EmailService.ts
│   │   └── StripePaymentService.ts
│   ├── database/         # データベース設定
│   │   ├── prisma/
│   │   └── migrations/
│   └── config/           # 設定ファイル
│       └── database.config.ts
├── presentation/          # プレゼンテーション層
│   ├── controllers/      # コントローラー
│   │   ├── UserController.ts
│   │   └── OrderController.ts
│   ├── routes/           # ルーティング
│   │   └── index.ts
│   └── middleware/       # ミドルウェア
│       ├── auth.ts
│       └── validation.ts
└── tests/                # テスト
    ├── unit/
    ├── integration/
    └── e2e/

実装の進捗管理

Clean Architecture の実装を段階的に進める際の推奨アプローチです。

ドメイン層の設計

エンティティとビジネスルールの定義

ユースケースの実装

アプリケーションロジックの実装

インターフェースアダプターの作成

コントローラーとプレゼンターの実装

インフラストラクチャの実装

データベースと外部サービスの統合

テストの充実

ユニットテストと統合テストの実装

パフォーマンスとスケーラビリティ

Clean Architecture は保守性を重視する設計ですが、パフォーマンスへの配慮も重要です。

キャッシング戦略

// infrastructure/cache/RedisCacheService.ts
export class RedisCacheService implements ICacheService {
  constructor(private readonly redis: Redis) {}

  async get<T>(key: string): Promise<T | null> {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const data = JSON.stringify(value);
    if (ttl) {
      await this.redis.setex(key, ttl, data);
    } else {
      await this.redis.set(key, data);
    }
  }

  async delete(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

// application/usecases/GetUserUseCase.ts
export class GetUserUseCase {
  constructor(
    private readonly userRepository: IUserRepository,
    private readonly cacheService: ICacheService
  ) {}

  async execute(userId: string): Promise<User> {
    // キャッシュチェック
    const cacheKey = `user:${userId}`;
    const cachedUser = await this.cacheService.get<User>(cacheKey);
    
    if (cachedUser) {
      return cachedUser;
    }

    // データベースから取得
    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new Error('ユーザーが見つかりません');
    }

    // キャッシュに保存(TTL: 1時間)
    await this.cacheService.set(cacheKey, user, 3600);

    return user;
  }
}

トラブルシューティングガイド

Clean Architecture 実装時によく遭遇する問題と解決策を紹介します。

よくある実装上の問題

Clean Architecture実装時の一般的な問題
問題 症状 原因 解決策
循環依存 コンパイルエラー、import cycle 層の境界が曖昧 インターフェースの適切な配置、依存性の方向を再確認
過度な抽象化 単純な処理が複雑に 原則の過剰適用 規模に応じた適切なレベルの抽象化
パフォーマンス劣化 レスポンス時間の増加 過度な層の分離 クリティカルパスの最適化、適切なキャッシング
テストの複雑化 モックが大量に必要 インターフェースの粒度が細かすぎる インターフェースの統合、テストヘルパーの作成

循環依存の解決例

// ❌ 循環依存が発生するパターン
// domain/services/UserService.ts
import { UserRepository } from '../../infrastructure/repositories/UserRepository';

// infrastructure/repositories/UserRepository.ts  
import { UserService } from '../../domain/services/UserService';

// ✅ インターフェースで解決
// domain/repositories/IUserRepository.ts
export interface IUserRepository {
  findById(id: string): Promise<User | null>;
}

// domain/services/UserService.ts
import { IUserRepository } from '../repositories/IUserRepository';

export class UserService {
  constructor(private readonly userRepository: IUserRepository) {}
}

// infrastructure/repositories/UserRepository.ts
import { IUserRepository } from '../../domain/repositories/IUserRepository';

export class UserRepository implements IUserRepository {
  // 実装
}

パフォーマンス最適化のテクニック

1. 遅延読み込みとキャッシング

// application/usecases/GetProductsUseCase.ts
export class GetProductsUseCase {
  private cache = new Map<string, CacheEntry<Product[]>>();
  
  constructor(
    private readonly productRepository: IProductRepository,
    private readonly cacheService: ICacheService
  ) {}

  async execute(params: GetProductsParams): Promise<Product[]> {
    const cacheKey = this.generateCacheKey(params);
    
    // メモリキャッシュチェック
    const memoryCache = this.cache.get(cacheKey);
    if (memoryCache && !this.isExpired(memoryCache)) {
      return memoryCache.data;
    }
    
    // Redis キャッシュチェック
    const redisCache = await this.cacheService.get<Product[]>(cacheKey);
    if (redisCache) {
      this.cache.set(cacheKey, {
        data: redisCache,
        timestamp: Date.now()
      });
      return redisCache;
    }
    
    // データベースから取得
    const products = await this.productRepository.findByParams(params);
    
    // キャッシュに保存(非同期)
    this.saveToCache(cacheKey, products);
    
    return products;
  }
  
  private async saveToCache(key: string, data: Product[]): Promise<void> {
    // メモリキャッシュ
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
    
    // Redisキャッシュ(非ブロッキング)
    this.cacheService.set(key, data, 3600).catch(err => {
      console.error('Cache save error:', err);
    });
  }
  
  private isExpired(entry: CacheEntry<any>): boolean {
    return Date.now() - entry.timestamp > 60000; // 1分
  }
}

2. バッチ処理とバルク操作

// application/usecases/BulkCreateOrdersUseCase.ts
export class BulkCreateOrdersUseCase {
  constructor(
    private readonly orderRepository: IOrderRepository,
    private readonly inventoryService: IInventoryService,
    private readonly eventBus: IEventBus
  ) {}

  async execute(orderInputs: CreateOrderInput[]): Promise<BulkCreateResult> {
    const results: OrderResult[] = [];
    const batchSize = 100;
    
    // バッチ処理で効率化
    for (let i = 0; i < orderInputs.length; i += batchSize) {
      const batch = orderInputs.slice(i, i + batchSize);
      
      // 並列処理
      const batchResults = await Promise.all(
        batch.map(async (input) => {
          try {
            // 在庫の一括確認
            const inventory = await this.inventoryService.checkBulk(
              input.items.map(item => ({
                productId: item.productId,
                quantity: item.quantity
              }))
            );
            
            if (!inventory.allAvailable) {
              return {
                success: false,
                orderId: null,
                error: '在庫不足',
                input
              };
            }
            
            // 注文作成
            const order = await this.createOrder(input);
            
            return {
              success: true,
              orderId: order.id,
              error: null,
              input
            };
          } catch (error) {
            return {
              success: false,
              orderId: null,
              error: error.message,
              input
            };
          }
        })
      );
      
      results.push(...batchResults);
      
      // バッチごとにイベント発行
      await this.eventBus.publish(
        new BulkOrdersProcessedEvent(batchResults)
      );
    }
    
    return {
      total: orderInputs.length,
      successful: results.filter(r => r.success).length,
      failed: results.filter(r => !r.success).length,
      results
    };
  }
}

3. 非同期処理とイベント駆動

// domain/events/OrderEvents.ts
export class OrderCreatedEvent {
  constructor(
    public readonly orderId: string,
    public readonly customerId: string,
    public readonly totalAmount: number,
    public readonly items: OrderItem[]
  ) {}
}

// application/eventHandlers/OrderEventHandler.ts
export class OrderEventHandler {
  constructor(
    private readonly emailService: IEmailService,
    private readonly analyticsService: IAnalyticsService,
    private readonly inventoryService: IInventoryService
  ) {}

  @EventHandler(OrderCreatedEvent)
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    // 非同期で並列処理
    await Promise.allSettled([
      this.sendOrderConfirmationEmail(event),
      this.trackOrderAnalytics(event),
      this.updateInventory(event)
    ]);
  }
  
  private async sendOrderConfirmationEmail(event: OrderCreatedEvent): Promise<void> {
    try {
      await this.emailService.sendOrderConfirmation({
        orderId: event.orderId,
        customerId: event.customerId,
        amount: event.totalAmount
      });
    } catch (error) {
      // エラーログだけ記録し、処理は継続
      console.error('Email sending failed:', error);
    }
  }
}

実装時の注意点とベストプラクティス

1. エラーハンドリングの統一

// domain/errors/DomainErrors.ts
export abstract class DomainError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 400
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

export class ValidationError extends DomainError {
  constructor(message: string, public readonly fields?: Record<string, string[]>) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

export class NotFoundError extends DomainError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
  }
}

export class BusinessRuleViolationError extends DomainError {
  constructor(message: string) {
    super(message, 'BUSINESS_RULE_VIOLATION', 422);
  }
}

// application/usecases/BaseUseCase.ts
export abstract class BaseUseCase<TInput, TOutput> {
  protected abstract validate(input: TInput): Promise<void>;
  protected abstract process(input: TInput): Promise<TOutput>;
  
  async execute(input: TInput): Promise<TOutput> {
    try {
      await this.validate(input);
      return await this.process(input);
    } catch (error) {
      if (error instanceof DomainError) {
        throw error;
      }
      
      // 予期しないエラーをラップ
      throw new DomainError(
        'An unexpected error occurred',
        'INTERNAL_ERROR',
        500
      );
    }
  }
}

2. 依存性注入コンテナの活用

// infrastructure/di/Container.ts
import { Container } from 'inversify';
import { TYPES } from './types';

export class DIContainer {
  private container: Container;
  
  constructor() {
    this.container = new Container();
    this.registerServices();
  }
  
  private registerServices(): void {
    // Repositories
    this.container.bind<IUserRepository>(TYPES.UserRepository)
      .to(UserRepository)
      .inSingletonScope();
      
    this.container.bind<IOrderRepository>(TYPES.OrderRepository)
      .to(OrderRepository)
      .inSingletonScope();
    
    // Services
    this.container.bind<IEmailService>(TYPES.EmailService)
      .to(EmailService)
      .inSingletonScope();
      
    this.container.bind<ICacheService>(TYPES.CacheService)
      .to(RedisCacheService)
      .inSingletonScope();
    
    // Use Cases
    this.container.bind<CreateUserUseCase>(TYPES.CreateUserUseCase)
      .to(CreateUserUseCase)
      .inTransientScope();
      
    this.container.bind<CreateOrderUseCase>(TYPES.CreateOrderUseCase)
      .to(CreateOrderUseCase)
      .inTransientScope();
  }
  
  get<T>(serviceIdentifier: symbol): T {
    return this.container.get<T>(serviceIdentifier);
  }
}

// 使用例
const container = new DIContainer();
const createUserUseCase = container.get<CreateUserUseCase>(TYPES.CreateUserUseCase);

関連技術との統合

Domain-Driven Design (DDD) との組み合わせ

// domain/aggregates/Order.ts
export class Order {
  private items: OrderItem[] = [];
  private events: DomainEvent[] = [];
  
  constructor(
    private readonly id: OrderId,
    private readonly customerId: CustomerId,
    private status: OrderStatus
  ) {}
  
  static create(customerId: CustomerId): Order {
    const order = new Order(
      OrderId.generate(),
      customerId,
      OrderStatus.PENDING
    );
    
    order.addEvent(new OrderCreatedEvent(order.id.value, customerId.value));
    return order;
  }
  
  addItem(product: Product, quantity: Quantity): void {
    // ビジネスルールの検証
    if (this.status !== OrderStatus.PENDING) {
      throw new BusinessRuleViolationError(
        '確定済みの注文には商品を追加できません'
      );
    }
    
    if (quantity.value > product.maxOrderQuantity) {
      throw new BusinessRuleViolationError(
        `最大注文数量(${product.maxOrderQuantity})を超えています`
      );
    }
    
    const existingItem = this.items.find(
      item => item.productId.equals(product.id)
    );
    
    if (existingItem) {
      existingItem.increaseQuantity(quantity);
    } else {
      this.items.push(new OrderItem(product.id, quantity, product.price));
    }
    
    this.addEvent(new OrderItemAddedEvent(
      this.id.value,
      product.id.value,
      quantity.value
    ));
  }
  
  getUncommittedEvents(): DomainEvent[] {
    return [...this.events];
  }
  
  markEventsAsCommitted(): void {
    this.events = [];
  }
}

マイクロサービスアーキテクチャへの適用

// infrastructure/messaging/EventBusAdapter.ts
export class EventBusAdapter implements IEventBus {
  constructor(
    private readonly kafkaProducer: KafkaProducer,
    private readonly schemaRegistry: SchemaRegistry
  ) {}
  
  async publish(event: DomainEvent): Promise<void> {
    const schema = await this.schemaRegistry.getSchema(event.constructor.name);
    const payload = this.serialize(event, schema);
    
    await this.kafkaProducer.send({
      topic: this.getTopicName(event),
      messages: [{
        key: event.aggregateId,
        value: payload,
        headers: {
          eventType: event.constructor.name,
          version: schema.version,
          timestamp: Date.now().toString()
        }
      }]
    });
  }
  
  private getTopicName(event: DomainEvent): string {
    // イベントタイプからトピック名を生成
    return `domain.events.${event.constructor.name.toLowerCase()}`;
  }
}

まとめ

Clean Architecture は、ソフトウェアの保守性と拡張性を大幅に向上させる強力な設計パターンです。本記事で解説した内容をまとめます:

Clean Architecture実装のポイント

  • 依存性の方向を内側に向ける:外側の層は内側の層に依存するが、逆は成立しない
  • インターフェースで抽象化:具体的な実装はインターフェースを通じて注入
  • ビジネスロジックの独立性:エンティティとユースケースは外部技術から独立
  • テスタビリティの確保:各層が独立しているため、モックを使ったテストが容易
  • 段階的な実装:既存システムにも段階的に適用可能
  • パフォーマンスの考慮:適切なキャッシング戦略とバッチ処理の実装
  • エラーハンドリング:ドメインエラーの統一的な管理
  • 他の設計パターンとの統合:DDD、マイクロサービスとの相性も良好
Clean Architecture習得度 100 %
完了

Clean Architecture は学習曲線が高いですが、一度理解すれば、保守性の高い堅牢なシステムを構築できます。実際のプロジェクトでは、チームの技術レベルやプロジェクトの規模に応じて、適切なレベルで適用することが重要です。

本記事で紹介したトラブルシューティングやパフォーマンス最適化のテクニックを活用することで、実践的な Clean Architecture の実装が可能になります。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。