ドメイン駆動設計(DDD)による責任分離

DDDの核心

ドメイン駆動設計は、ビジネスの複雑性を管理するために、ドメインモデルを中心に据えたソフトウェア設計手法です。

DDDの主要概念

  • ユビキタス言語:開発者とドメインエキスパートが共有する言語
  • 境界づけられたコンテキスト:モデルが適用される明確な境界
  • エンティティ:識別子を持つオブジェクト
  • 値オブジェクト:不変で識別子を持たないオブジェクト
  • 集約:整合性の境界

境界づけられたコンテキスト

コンテキストの分離

大規模システムを、それぞれが独自のモデルを持つ複数のコンテキストに分割します。

// ECサイトの境界づけられたコンテキストの例

// 販売コンテキスト
namespace SalesContext {
    interface Product {
        id: ProductId;
        name: string;
        price: Money;
        description: string;
    }
    
    interface Order {
        id: OrderId;
        customerId: CustomerId;
        items: OrderItem[];
        totalAmount: Money;
        status: OrderStatus;
    }
}

// 在庫コンテキスト
namespace InventoryContext {
    interface Product {
        id: ProductId;
        sku: string;
        quantity: number;
        location: WarehouseLocation;
    }
    
    interface StockMovement {
        productId: ProductId;
        quantity: number;
        type: 'in' | 'out';
        timestamp: Date;
    }
}

// 配送コンテキスト
namespace ShippingContext {
    interface Shipment {
        id: ShipmentId;
        orderId: OrderId;
        items: ShipmentItem[];
        destination: Address;
        carrier: Carrier;
        trackingNumber: string;
    }
}

コンテキスト間の統合

  • 各コンテキストは独立して進化できる
  • 同じ概念(Product)でも、コンテキストによって異なる属性を持つ
  • イベント駆動やAPIを通じて連携

レイヤードアーキテクチャ

責任の層別分離

// プレゼンテーション層
// UIに関する責任
class OrderController {
    constructor(private orderService: OrderService) {}
    
    async createOrder(req: Request, res: Response) {
        try {
            const orderData = req.body;
            const order = await this.orderService.createOrder(orderData);
            res.json({ success: true, orderId: order.id });
        } catch (error) {
            res.status(400).json({ error: error.message });
        }
    }
}

// アプリケーション層
// ユースケースの調整責任
class OrderService {
    constructor(
        private orderRepository: OrderRepository,
        private inventoryService: InventoryService,
        private paymentService: PaymentService
    ) {}
    
    async createOrder(orderData: CreateOrderDTO): Promise {
        // 在庫確認
        await this.inventoryService.checkAvailability(orderData.items);
        
        // ドメインオブジェクトの生成
        const order = Order.create(orderData);
        
        // 支払い処理
        await this.paymentService.processPayment(order.totalAmount);
        
        // 永続化
        await this.orderRepository.save(order);
        
        return order;
    }
}

// ドメイン層
// ビジネスロジックの責任
class Order {
    private constructor(
        private id: OrderId,
        private customerId: CustomerId,
        private items: OrderItem[],
        private status: OrderStatus
    ) {}
    
    static create(data: CreateOrderData): Order {
        if (data.items.length === 0) {
            throw new Error('Order must have at least one item');
        }
        
        const items = data.items.map(item => 
            new OrderItem(item.productId, item.quantity, item.price)
        );
        
        return new Order(
            OrderId.generate(),
            data.customerId,
            items,
            OrderStatus.PENDING
        );
    }
    
    get totalAmount(): Money {
        return this.items.reduce(
            (total, item) => total.add(item.subtotal),
            Money.zero()
        );
    }
    
    confirm(): void {
        if (this.status !== OrderStatus.PENDING) {
            throw new Error('Only pending orders can be confirmed');
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

// インフラストラクチャ層
// 技術的詳細の責任
class PostgresOrderRepository implements OrderRepository {
    async save(order: Order): Promise {
        const data = this.mapToDatabase(order);
        await db.query(
            'INSERT INTO orders (id, customer_id, items, status) VALUES ($1, $2, $3, $4)',
            [data.id, data.customerId, JSON.stringify(data.items), data.status]
        );
    }
    
    private mapToDatabase(order: Order): any {
        // ドメインオブジェクトをDBスキーマにマッピング
    }
}

ヘキサゴナルアーキテクチャ

ポートとアダプター

ビジネスロジックを中心に据え、外部システムとの接続をポートとアダプターで抽象化します。

// ポート(ビジネスロジックが定義するインターフェース)
// 入力ポート
interface CreateOrderUseCase {
    execute(command: CreateOrderCommand): Promise;
}

// 出力ポート
interface OrderRepository {
    save(order: Order): Promise;
    findById(id: OrderId): Promise;
}

interface PaymentGateway {
    charge(amount: Money, paymentMethod: PaymentMethod): Promise;
}

interface NotificationService {
    sendOrderConfirmation(email: Email, order: Order): Promise;
}

// ビジネスロジック(ヘキサゴンの中心)
class CreateOrderService implements CreateOrderUseCase {
    constructor(
        private orderRepo: OrderRepository,
        private paymentGateway: PaymentGateway,
        private notificationService: NotificationService
    ) {}
    
    async execute(command: CreateOrderCommand): Promise {
        // ビジネスロジックの実行
        const order = Order.create(command);
        
        // 支払い処理(出力ポートを通じて)
        const paymentResult = await this.paymentGateway.charge(
            order.totalAmount,
            command.paymentMethod
        );
        
        order.confirmPayment(paymentResult);
        
        // 永続化(出力ポートを通じて)
        await this.orderRepo.save(order);
        
        // 通知(出力ポートを通じて)
        await this.notificationService.sendOrderConfirmation(
            command.customerEmail,
            order
        );
        
        return new OrderCreatedEvent(order);
    }
}

// アダプター(外部システムとの接続実装)
// 入力アダプター(REST API)
class OrderRestController {
    constructor(private createOrderUseCase: CreateOrderUseCase) {}
    
    async createOrder(req: Request, res: Response) {
        const command = this.mapToCommand(req.body);
        const event = await this.createOrderUseCase.execute(command);
        res.json(this.mapToResponse(event));
    }
}

// 出力アダプター(データベース)
class PostgresOrderRepository implements OrderRepository {
    async save(order: Order): Promise {
        // PostgreSQL固有の実装
    }
}

// 出力アダプター(決済サービス)
class StripePaymentGateway implements PaymentGateway {
    async charge(amount: Money, paymentMethod: PaymentMethod): Promise {
        // Stripe API呼び出し
    }
}

マイクロサービスへの適用

サービス境界の設計

責任分離の原則をサービスレベルで適用し、独立してデプロイ可能なサービスを構築します。

サービス分割の指針

  • ビジネス機能による分割(機能的凝集)
  • データの所有権による分割
  • チームの責任範囲による分割
  • 変更の頻度による分割
// サービス間通信の例
// 注文サービス
class OrderService {
    async createOrder(orderData: CreateOrderDTO) {
        // 在庫サービスに在庫確認
        const available = await this.inventoryClient
            .checkAvailability(orderData.items);
        
        if (!available) {
            throw new Error('Insufficient inventory');
        }
        
        const order = await this.orderRepository.create(orderData);
        
        // イベントを発行
        await this.eventBus.publish(new OrderCreatedEvent({
            orderId: order.id,
            items: order.items,
            customerId: order.customerId
        }));
        
        return order;
    }
}

// 在庫サービス(イベントを購読)
class InventoryService {
    constructor() {
        this.eventBus.subscribe('OrderCreated', this.handleOrderCreated);
    }
    
    async handleOrderCreated(event: OrderCreatedEvent) {
        // 在庫を減らす
        for (const item of event.items) {
            await this.reduceStock(item.productId, item.quantity);
        }
    }
}

アーキテクチャ設計の原則

大規模システムでの責任分離

  • 高凝集・疎結合を維持する
  • 明確な境界を定義する
  • 共有データベースを避ける
  • 同期的な依存を最小化する
  • イベント駆動で疎結合を実現する
  • 各サービスは独自のデータストアを持つ