森 宝松 SIer Tech Blog

SIer向け!業務アプリケーションの設計パターン大全

森 宝松
SIer Tech Blog
2025年3月15日

SIer向け!業務アプリケーションの設計パターン大全

業務アプリケーション開発において、適切な設計パターンの選択と適用は、プロジェクトの成功を左右する重要な要素です。本記事では、SIerエンジニアが知っておくべき業務アプリケーションの設計パターンを網羅的に解説します。基本的な概念から実践的な適用方法まで、豊富なコード例とともに紹介します。

1. 設計パターンの基礎と重要性

1.1 設計パターンとは

設計パターンとは、ソフトウェア設計において繰り返し発生する問題に対する、再利用可能な解決策のテンプレートです。これらは、長年の開発経験から得られたベストプラクティスを体系化したものであり、以下のような利点があります:

  • 効率的な開発: 既知の問題に対して検証済みの解決策を提供
  • コミュニケーションの円滑化: 開発者間で共通の語彙を提供
  • 品質の向上: 堅牢で保守性の高いコードの作成を支援
  • 再利用性の促進: コンポーネントの再利用を容易にする

1.2 業務アプリケーションにおける設計パターンの重要性

業務アプリケーションは、以下のような特性を持つことが多く、適切な設計パターンの適用が特に重要です:

  • 複雑なビジネスロジック: 業務ルールや計算が複雑
  • 長期的な保守: 数年から数十年にわたって運用・保守される
  • 多様なステークホルダー: 様々な部門や役割のユーザーが利用
  • 高い信頼性要件: データの正確性や処理の確実性が求められる
  • 変更の頻度: ビジネス要件の変更に迅速に対応する必要がある

1.3 設計パターンの分類

設計パターンは一般的に以下のように分類されます:

  1. 創造的パターン (Creational Patterns):

    • オブジェクトの作成メカニズムに関するパターン
    • 例: Factory Method, Abstract Factory, Singleton, Builder
  2. 構造的パターン (Structural Patterns):

    • クラスやオブジェクトの構成に関するパターン
    • 例: Adapter, Bridge, Composite, Decorator, Facade
  3. 振る舞い的パターン (Behavioral Patterns):

    • オブジェクト間の通信や責任の分配に関するパターン
    • 例: Observer, Strategy, Command, Template Method
  4. アーキテクチャパターン:

    • システム全体の構造に関するパターン
    • 例: MVC, MVVM, Layered Architecture, Microservices

2. 業務アプリケーションのアーキテクチャパターン

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

レイヤードアーキテクチャは、業務アプリケーションで最も広く使用されているアーキテクチャパターンの一つです。機能を論理的な層に分割し、各層が特定の責任を持ちます。

2.1.1 基本構造

一般的な4層構造:

  1. プレゼンテーション層: ユーザーインターフェースとユーザー操作の処理
  2. アプリケーション層: ビジネスプロセスの調整とユースケースの実装
  3. ドメイン層: ビジネスロジックとドメインモデルの実装
  4. インフラストラクチャ層: データアクセス、外部サービス連携、技術的な実装

2.1.2 実装例 (Java)

// プレゼンテーション層
@Controller
public class OrderController {
    private final OrderService orderService;
    
    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    @PostMapping("/orders")
    public String createOrder(@ModelAttribute OrderForm form, Model model) {
        try {
            OrderDTO order = orderService.createOrder(form.toDTO());
            model.addAttribute("order", order);
            return "order/confirmation";
        } catch (Exception e) {
            model.addAttribute("error", e.getMessage());
            return "order/form";
        }
    }
}

// アプリケーション層
@Service
public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final ProductRepository productRepository;
    
    @Autowired
    public OrderServiceImpl(
            OrderRepository orderRepository,
            CustomerRepository customerRepository,
            ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
        this.productRepository = productRepository;
    }
    
    @Transactional
    @Override
    public OrderDTO createOrder(OrderDTO orderDTO) {
        // 顧客の存在確認
        Customer customer = customerRepository.findById(orderDTO.getCustomerId())
                .orElseThrow(() -> new EntityNotFoundException("Customer not found"));
        
        // 注文エンティティの作成
        Order order = new Order();
        order.setCustomer(customer);
        order.setOrderDate(LocalDateTime.now());
        order.setStatus(OrderStatus.PENDING);
        
        // 注文明細の作成
        for (OrderItemDTO itemDTO : orderDTO.getItems()) {
            Product product = productRepository.findById(itemDTO.getProductId())
                    .orElseThrow(() -> new EntityNotFoundException("Product not found"));
            
            OrderItem item = new OrderItem();
            item.setOrder(order);
            item.setProduct(product);
            item.setQuantity(itemDTO.getQuantity());
            item.setUnitPrice(product.getPrice());
            
            order.addItem(item);
        }
        
        // 合計金額の計算
        order.calculateTotal();
        
        // 注文の保存
        Order savedOrder = orderRepository.save(order);
        
        return OrderMapper.toDTO(savedOrder);
    }
}

// ドメイン層
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;
    
    @Column(name = "order_date", nullable = false)
    private LocalDateTime orderDate;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private OrderStatus status;
    
    @Column(name = "total_amount")
    private BigDecimal totalAmount;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
    
    // ビジネスロジック
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
    
    public void calculateTotal() {
        totalAmount = items.stream()
                .map(item -> item.getUnitPrice().multiply(new BigDecimal(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // getters and setters
}

// インフラストラクチャ層
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerId(Long customerId);
    List<Order> findByStatusAndOrderDateBetween(OrderStatus status, LocalDateTime start, LocalDateTime end);
}

2.1.3 メリットとデメリット

メリット:

  • 責任の明確な分離
  • 各層の独立した開発とテスト
  • 保守性と拡張性の向上
  • チーム間の並行開発の容易化

デメリット:

  • オーバーヘッドの増加(特に小規模なアプリケーションの場合)
  • 過度に複雑になる可能性
  • パフォーマンスへの影響(層間の変換処理など)

2.2 ヘキサゴナルアーキテクチャ(ポートとアダプター)

ヘキサゴナルアーキテクチャは、アプリケーションのコアロジックを外部の依存関係から分離することに焦点を当てたアーキテクチャパターンです。

2.2.1 基本構造

  • ドメインコア: ビジネスロジックとドメインモデル
  • ポート: アプリケーションとの通信インターフェース
    • 入力ポート: アプリケーションが提供するサービス
    • 出力ポート: アプリケーションが必要とする外部サービス
  • アダプター: ポートの実装
    • 入力アダプター: UI、API、メッセージキューなど
    • 出力アダプター: データベース、外部APIクライアントなど

2.2.2 実装例 (Java)

// ドメインモデル
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private Money totalAmount;
    
    // ビジネスロジック
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(product.getId(), quantity, product.getPrice());
        items.add(item);
        recalculateTotal();
    }
    
    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm an empty order");
        }
        status = OrderStatus.CONFIRMED;
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
                .map(item -> item.getPrice().multiply(item.getQuantity()))
                .reduce(Money.ZERO, Money::add);
    }
    
    // getters
}

// 入力ポート(アプリケーションサービスインターフェース)
public interface OrderService {
    OrderId createOrder(CustomerId customerId);
    void addOrderItem(OrderId orderId, ProductId productId, int quantity);
    void confirmOrder(OrderId orderId);
    Order getOrder(OrderId orderId);
}

// 出力ポート(リポジトリインターフェース)
public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId orderId);
}

// アプリケーションサービス(入力ポートの実装)
public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    public OrderServiceImpl(OrderRepository orderRepository, ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }
    
    @Override
    public OrderId createOrder(CustomerId customerId) {
        Order order = new Order(OrderId.generate(), customerId);
        orderRepository.save(order);
        return order.getId();
    }
    
    @Override
    public void addOrderItem(OrderId orderId, ProductId productId, int quantity) {
        Order order = orderRepository.findById(orderId);
        Product product = productRepository.findById(productId);
        
        order.addItem(product, quantity);
        orderRepository.save(order);
    }
    
    @Override
    public void confirmOrder(OrderId orderId) {
        Order order = orderRepository.findById(orderId);
        order.confirm();
        orderRepository.save(order);
    }
    
    @Override
    public Order getOrder(OrderId orderId) {
        return orderRepository.findById(orderId);
    }
}

// 入力アダプター(REST API)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;
    
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        CustomerId customerId = new CustomerId(request.getCustomerId());
        OrderId orderId = orderService.createOrder(customerId);
        
        for (OrderItemRequest itemRequest : request.getItems()) {
            ProductId productId = new ProductId(itemRequest.getProductId());
            orderService.addOrderItem(orderId, productId, itemRequest.getQuantity());
        }
        
        orderService.confirmOrder(orderId);
        Order order = orderService.getOrder(orderId);
        
        return ResponseEntity.ok(OrderResponse.fromOrder(order));
    }
}

// 出力アダプター(JPA実装)
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final OrderJpaRepository jpaRepository;
    private final OrderEntityMapper mapper;
    
    public JpaOrderRepository(OrderJpaRepository jpaRepository, OrderEntityMapper mapper) {
        this.jpaRepository = jpaRepository;
        this.mapper = mapper;
    }
    
    @Override
    public void save(Order order) {
        OrderEntity entity = mapper.toEntity(order);
        jpaRepository.save(entity);
    }
    
    @Override
    public Order findById(OrderId orderId) {
        OrderEntity entity = jpaRepository.findById(orderId.getValue())
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
        return mapper.toDomain(entity);
    }
}

2.2.3 メリットとデメリット

メリット:

  • ドメインロジックの分離と保護
  • 外部依存関係の交換容易性
  • テスト容易性の向上
  • 技術的な詳細からビジネスロジックを分離

デメリット:

  • 追加的な抽象化レイヤーによる複雑さ
  • 多くのインターフェースとマッピングが必要
  • 小規模なアプリケーションでは過剰な場合がある

2.3 クリーンアーキテクチャ

クリーンアーキテクチャは、ロバート・C・マーティンによって提唱されたアーキテクチャパターンで、ヘキサゴナルアーキテクチャと多くの共通点を持ちます。

2.3.1 基本構造

クリーンアーキテクチャは、以下の同心円状の層で構成されます:

  1. エンティティ層: ビジネスエンティティとコアビジネスルール
  2. ユースケース層: アプリケーション固有のビジネスルールとユースケース
  3. インターフェースアダプター層: コントローラー、プレゼンター、ゲートウェイなど
  4. フレームワークと外部ツール層: データベース、Webフレームワーク、外部APIなど

重要な原則は「依存関係の方向」で、内側の層は外側の層に依存してはならず、依存の方向は常に内側に向かいます。

2.3.2 実装例 (Java)

// エンティティ層
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderItem> items;
    private OrderStatus status;
    private Money totalAmount;
    
    public Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.items = new ArrayList<>();
        this.status = OrderStatus.DRAFT;
        this.totalAmount = Money.ZERO;
    }
    
    public void addItem(OrderItem item) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot add items to a non-draft order");
        }
        items.add(item);
        recalculateTotal();
    }
    
    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm an empty order");
        }
        status = OrderStatus.CONFIRMED;
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(Money.ZERO, Money::add);
    }
    
    // getters
}

// ユースケース層
public class CreateOrderUseCase implements UseCase<CreateOrderRequest, CreateOrderResponse> {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    public CreateOrderUseCase(OrderRepository orderRepository, ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }
    
    @Override
    public CreateOrderResponse execute(CreateOrderRequest request) {
        // 注文の作成
        Order order = new Order(
                new OrderId(UUID.randomUUID().toString()),
                new CustomerId(request.getCustomerId())
        );
        
        // 注文明細の追加
        for (OrderItemRequest itemRequest : request.getItems()) {
            Product product = productRepository.findById(new ProductId(itemRequest.getProductId()));
            if (product == null) {
                throw new EntityNotFoundException("Product not found");
            }
            
            OrderItem item = new OrderItem(
                    new OrderItemId(UUID.randomUUID().toString()),
                    product.getId(),
                    itemRequest.getQuantity(),
                    product.getPrice()
            );
            
            order.addItem(item);
        }
        
        // 注文の確定
        order.confirm();
        
        // 注文の保存
        orderRepository.save(order);
        
        // レスポンスの作成
        return new CreateOrderResponse(
                order.getId().getValue(),
                order.getStatus().name(),
                order.getTotalAmount().getValue()
        );
    }
}

// インターフェースアダプター層 - コントローラー
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final CreateOrderUseCase createOrderUseCase;
    
    public OrderController(CreateOrderUseCase createOrderUseCase) {
        this.createOrderUseCase = createOrderUseCase;
    }
    
    @PostMapping
    public ResponseEntity<CreateOrderResponseDto> createOrder(@RequestBody CreateOrderRequestDto requestDto) {
        // DTOからユースケースリクエストへの変換
        CreateOrderRequest request = new CreateOrderRequest(
                requestDto.getCustomerId(),
                requestDto.getItems().stream()
                        .map(item -> new OrderItemRequest(item.getProductId(), item.getQuantity()))
                        .collect(Collectors.toList())
        );
        
        // ユースケースの実行
        CreateOrderResponse response = createOrderUseCase.execute(request);
        
        // レスポンスDTOの作成
        CreateOrderResponseDto responseDto = new CreateOrderResponseDto(
                response.getOrderId(),
                response.getStatus(),
                response.getTotalAmount()
        );
        
        return ResponseEntity.ok(responseDto);
    }
}

// インターフェースアダプター層 - リポジトリ実装
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final OrderJpaRepository jpaRepository;
    private final OrderItemJpaRepository orderItemJpaRepository;
    
    public JpaOrderRepository(OrderJpaRepository jpaRepository, OrderItemJpaRepository orderItemJpaRepository) {
        this.jpaRepository = jpaRepository;
        this.orderItemJpaRepository = orderItemJpaRepository;
    }
    
    @Override
    public void save(Order order) {
        // ドメインモデルからJPAエンティティへの変換
        OrderJpaEntity orderEntity = new OrderJpaEntity();
        orderEntity.setId(order.getId().getValue());
        orderEntity.setCustomerId(order.getCustomerId().getValue());
        orderEntity.setStatus(order.getStatus().name());
        orderEntity.setTotalAmount(order.getTotalAmount().getValue());
        
        // 注文の保存
        jpaRepository.save(orderEntity);
        
        // 注文明細の保存
        for (OrderItem item : order.getItems()) {
            OrderItemJpaEntity itemEntity = new OrderItemJpaEntity();
            itemEntity.setId(item.getId().getValue());
            itemEntity.setOrderId(order.getId().getValue());
            itemEntity.setProductId(item.getProductId().getValue());
            itemEntity.setQuantity(item.getQuantity());
            itemEntity.setUnitPrice(item.getUnitPrice().getValue());
            
            orderItemJpaRepository.save(itemEntity);
        }
    }
    
    @Override
    public Order findById(OrderId orderId) {
        // JPAエンティティの取得
        OrderJpaEntity orderEntity = jpaRepository.findById(orderId.getValue())
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
        
        List<OrderItemJpaEntity> itemEntities = orderItemJpaRepository.findByOrderId(orderId.getValue());
        
        // JPAエンティティからドメインモデルへの変換
        Order order = new Order(
                new OrderId(orderEntity.getId()),
                new CustomerId(orderEntity.getCustomerId())
        );
        
        // 注文ステータスの設定
        OrderStatus status = OrderStatus.valueOf(orderEntity.getStatus());
        order.setStatus(status);
        
        // 注文明細の設定
        for (OrderItemJpaEntity itemEntity : itemEntities) {
            OrderItem item = new OrderItem(
                    new OrderItemId(itemEntity.getId()),
                    new ProductId(itemEntity.getProductId()),
                    itemEntity.getQuantity(),
                    new Money(itemEntity.getUnitPrice())
            );
            order.addItem(item);
        }
        
        return order;
    }
}

2.3.3 メリットとデメリット

メリット:

  • 依存関係の明確な方向性
  • ドメインロジックの保護
  • フレームワークやデータベースからの独立性
  • テスト容易性の向上

デメリット:

  • 多くのクラスとインターフェースが必要
  • 初期設定の複雑さ
  • 学習曲線が急
  • 小規模なプロジェクトでは過剰な場合がある

2.4 マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャは、アプリケーションを独立して展開可能な小さなサービスの集合として構築するアプローチです。

2.4.1 基本構造

  • サービス分割: ビジネス機能に基づいて独立したサービスに分割
  • 独立したデータストア: 各サービスが独自のデータベースを持つ
  • API ゲートウェイ: クライアントリクエストのルーティングと集約
  • サービス間通信: REST、gRPC、メッセージキューなど
  • 分散トランザクション: Saga パターンなどによる一貫性の確保

2.4.2 実装例 (Spring Boot マイクロサービス)

注文サービス:

// 注文サービスのコントローラー
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;
    private final ProductServiceClient productServiceClient;
    
    public OrderController(OrderService orderService, ProductServiceClient productServiceClient) {
        this.orderService = orderService;
        this.productServiceClient = productServiceClient;
    }
    
    @PostMapping
    public ResponseEntity<OrderDTO> createOrder(@RequestBody CreateOrderRequest request) {
        // 商品情報の取得(商品サービスから)
        List<ProductDTO> products = new ArrayList<>();
        for (OrderItemRequest itemRequest : request.getItems()) {
            ProductDTO product = productServiceClient.getProduct(itemRequest.getProductId());
            products.add(product);
        }
        
        // 注文の作成
        OrderDTO order = orderService.createOrder(request.getCustomerId(), request.getItems(), products);
        
        return ResponseEntity.ok(order);
    }
    
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDTO> getOrder(@PathVariable String orderId) {
        OrderDTO order = orderService.getOrder(orderId);
        return ResponseEntity.ok(order);
    }
}

// 商品サービスクライアント(Feign Client)
@FeignClient(name = "product-service", fallback = ProductServiceClientFallback.class)
public interface ProductServiceClient {
    @GetMapping("/api/products/{productId}")
    ProductDTO getProduct(@PathVariable String productId);
}

// サーキットブレーカーのフォールバック
@Component
public class ProductServiceClientFallback implements ProductServiceClient {
    @Override
    public ProductDTO getProduct(String productId) {
        // フォールバック処理(例: キャッシュからの取得や、デフォルト値の返却)
        throw new ServiceUnavailableException("Product service is currently unavailable");
    }
}

商品サービス:

// 商品サービスのコントローラー
@RestController
@RequestMapping("/api/products")
public class ProductController {
    private final ProductService productService;
    
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping("/{productId}")
    public ResponseEntity<ProductDTO> getProduct(@PathVariable String productId) {
        ProductDTO product = productService.getProduct(productId);
        return ResponseEntity.ok(product);
    }
    
    @GetMapping
    public ResponseEntity<List<ProductDTO>> getAllProducts() {
        List<ProductDTO> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
    
    @PostMapping
    public ResponseEntity<ProductDTO> createProduct(@RequestBody CreateProductRequest request) {
        ProductDTO product = productService.createProduct(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(product);
    }
}

API ゲートウェイ (Spring Cloud Gateway):

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("order-service", r -> r.path("/api/orders/**")
                        .uri("lb://order-service"))
                .route("product-service", r -> r.path("/api/products/**")
                        .uri("lb://product-service"))
                .route("customer-service", r -> r.path("/api/customers/**")
                        .uri("lb://customer-service"))
                .build();
    }
    
    @Bean
    public GlobalFilter loggingFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            log.info("Path: {}, Method: {}", request.getPath(), request.getMethod());
            return chain.filter(exchange);
        };
    }
}

2.4.3 メリットとデメリット

メリット:

  • 独立した開発とデプロイ
  • 技術スタックの柔軟性
  • スケーラビリティの向上
  • 障害の分離
  • チーム編成の柔軟性

デメリット:

  • 分散システムの複雑さ
  • サービス間通信のオーバーヘッド
  • 一貫性の確保が難しい
  • 運用の複雑さ(モニタリング、デバッグなど)
  • 初期開発コストの増加

3. 業務アプリケーションの設計パターン

3.1 ドメインモデルパターン

ドメインモデルパターンは、ビジネスロジックをドメインオブジェクトに組み込むアプローチです。

3.1.1 基本概念

  • リッチドメインモデル: ビジネスロジックを含むドメインオブジェクト
  • エンティティ: 同一性を持つオブジェクト
  • 値オブジェクト: 属性のみで同一性を持たないオブジェクト
  • 集約: 一貫性の境界を定義するオブジェクトのクラスター
  • リポジトリ: 永続化を担当

3.1.2 実装例 (Java)

// エンティティ
public class Order {
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items = new ArrayList<>();
    private OrderStatus status;
    private Money totalAmount;
    private LocalDateTime createdAt;
    
    // ビジネスロジック
    public void addItem(Product product, int quantity) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot add items to a non-draft order");
        }
        
        // 既存の商品の場合は数量を増やす
        for (OrderItem item : items) {
            if (item.getProductId().equals(product.getId())) {
                item.increaseQuantity(quantity);
                recalculateTotal();
                return;
            }
        }
        
        // 新しい商品の場合は追加
        OrderItem newItem = new OrderItem(
                new OrderItemId(UUID.randomUUID().toString()),
                product.getId(),
                quantity,
                product.getPrice()
        );
        
        items.add(newItem);
        recalculateTotal();
    }
    
    public void removeItem(OrderItemId itemId) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot remove items from a non-draft order");
        }
        
        items.removeIf(item -> item.getId().equals(itemId));
        recalculateTotal();
    }
    
    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm an empty order");
        }
        
        status = OrderStatus.CONFIRMED;
    }
    
    public void cancel() {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel a shipped or delivered order");
        }
        
        status = OrderStatus.CANCELLED;
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(Money.ZERO, Money::add);
    }
    
    // getters and setters
}

// 値オブジェクト
public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("JPY"));
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount.setScale(2, RoundingMode.HALF_UP);
        this.currency = currency;
    }
    
    public Money(BigDecimal amount) {
        this(amount, Currency.getInstance("JPY"));
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add money with different currencies");
        }
        
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money multiply(int multiplier) {
        return new Money(this.amount.multiply(new BigDecimal(multiplier)), this.currency);
    }
    
    // equals, hashCode, toString
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount.equals(money.amount) && currency.equals(money.currency);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
    
    // getters
}

// リポジトリインターフェース
public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
    List<Order> findByStatus(OrderStatus status);
}

3.1.3 メリットとデメリット

メリット:

  • ビジネスロジックの一元管理
  • オブジェクト指向の原則に沿った設計
  • ドメインの表現力の向上
  • ビジネスルールの変更に強い

デメリット:

  • 学習曲線が急
  • パフォーマンスへの影響(複雑なオブジェクトグラフなど)
  • 永続化の複雑さ
  • 単純なCRUD操作には過剰な場合がある

3.2 トランザクションスクリプトパターン

トランザクションスクリプトパターンは、ビジネスロジックを手続き的なスクリプトとして実装するアプローチです。

3.2.1 基本概念

  • サービスクラス: ビジネスロジックを含む手続き的なメソッド
  • データアクセスオブジェクト (DAO): データベースアクセスを担当
  • データ転送オブジェクト (DTO): レイヤー間のデータ転送に使用

3.2.2 実装例 (Java)

// サービスクラス
@Service
public class OrderService {
    private final OrderDAO orderDAO;
    private final CustomerDAO customerDAO;
    private final ProductDAO productDAO;
    
    @Autowired
    public OrderService(OrderDAO orderDAO, CustomerDAO customerDAO, ProductDAO productDAO) {
        this.orderDAO = orderDAO;
        this.customerDAO = customerDAO;
        this.productDAO = productDAO;
    }
    
    @Transactional
    public OrderDTO createOrder(CreateOrderRequest request) {
        // 顧客の存在確認
        CustomerDTO customer = customerDAO.findById(request.getCustomerId());
        if (customer == null) {
            throw new EntityNotFoundException("Customer not found");
        }
        
        // 注文の作成
        OrderDTO order = new OrderDTO();
        order.setId(UUID.randomUUID().toString());
        order.setCustomerId(request.getCustomerId());
        order.setStatus("DRAFT");
        order.setCreatedAt(LocalDateTime.now());
        
        // 注文明細の作成と合計金額の計算
        List<OrderItemDTO> items = new ArrayList<>();
        BigDecimal totalAmount = BigDecimal.ZERO;
        
        for (OrderItemRequest itemRequest : request.getItems()) {
            // 商品の存在確認
            ProductDTO product = productDAO.findById(itemRequest.getProductId());
            if (product == null) {
                throw new EntityNotFoundException("Product not found");
            }
            
            // 注文明細の作成
            OrderItemDTO item = new OrderItemDTO();
            item.setId(UUID.randomUUID().toString());
            item.setOrderId(order.getId());
            item.setProductId(product.getId());
            item.setProductName(product.getName());
            item.setQuantity(itemRequest.getQuantity());
            item.setUnitPrice(product.getPrice());
            
            // 小計の計算
            BigDecimal subtotal = product.getPrice().multiply(new BigDecimal(itemRequest.getQuantity()));
            item.setSubtotal(subtotal);
            
            // 合計金額に加算
            totalAmount = totalAmount.add(subtotal);
            
            items.add(item);
        }
        
        order.setItems(items);
        order.setTotalAmount(totalAmount);
        
        // 注文の確定
        order.setStatus("CONFIRMED");
        
        // 注文の保存
        orderDAO.save(order);
        
        // 注文明細の保存
        for (OrderItemDTO item : items) {
            orderDAO.saveOrderItem(item);
        }
        
        return order;
    }
    
    @Transactional
    public void cancelOrder(String orderId) {
        OrderDTO order = orderDAO.findById(orderId);
        if (order == null) {
            throw new EntityNotFoundException("Order not found");
        }
        
        // 出荷済みまたは配送済みの注文はキャンセル不可
        if ("SHIPPED".equals(order.getStatus()) || "DELIVERED".equals(order.getStatus())) {
            throw new IllegalStateException("Cannot cancel a shipped or delivered order");
        }
        
        // 注文のキャンセル
        order.setStatus("CANCELLED");
        orderDAO.update(order);
    }
    
    public OrderDTO getOrder(String orderId) {
        OrderDTO order = orderDAO.findById(orderId);
        if (order == null) {
            throw new EntityNotFoundException("Order not found");
        }
        
        List<OrderItemDTO> items = orderDAO.findOrderItemsByOrderId(orderId);
        order.setItems(items);
        
        return order;
    }
    
    public List<OrderDTO> getOrdersByCustomerId(String customerId) {
        List<OrderDTO> orders = orderDAO.findByCustomerId(customerId);
        
        for (OrderDTO order : orders) {
            List<OrderItemDTO> items = orderDAO.findOrderItemsByOrderId(order.getId());
            order.setItems(items);
        }
        
        return orders;
    }
}

// データアクセスオブジェクト
@Repository
public class OrderDAO {
    private final JdbcTemplate jdbcTemplate;
    
    @Autowired
    public OrderDAO(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public void save(OrderDTO order) {
        String sql = "INSERT INTO orders (id, customer_id, status, total_amount, created_at) " +
                "VALUES (?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(sql,
                order.getId(),
                order.getCustomerId(),
                order.getStatus(),
                order.getTotalAmount(),
                order.getCreatedAt()
        );
    }
    
    public void saveOrderItem(OrderItemDTO item) {
        String sql = "INSERT INTO order_items (id, order_id, product_id, product_name, quantity, unit_price, subtotal) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(sql,
                item.getId(),
                item.getOrderId(),
                item.getProductId(),
                item.getProductName(),
                item.getQuantity(),
                item.getUnitPrice(),
                item.getSubtotal()
        );
    }
    
    public void update(OrderDTO order) {
        String sql = "UPDATE orders SET status = ?, total_amount = ? WHERE id = ?";
        
        jdbcTemplate.update(sql,
                order.getStatus(),
                order.getTotalAmount(),
                order.getId()
        );
    }
    
    public OrderDTO findById(String id) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at FROM orders WHERE id = ?";
        
        try {
            return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
                OrderDTO order = new OrderDTO();
                order.setId(rs.getString("id"));
                order.setCustomerId(rs.getString("customer_id"));
                order.setStatus(rs.getString("status"));
                order.setTotalAmount(rs.getBigDecimal("total_amount"));
                order.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
                return order;
            });
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }
    
    public List<OrderItemDTO> findOrderItemsByOrderId(String orderId) {
        String sql = "SELECT id, order_id, product_id, product_name, quantity, unit_price, subtotal " +
                "FROM order_items WHERE order_id = ?";
        
        return jdbcTemplate.query(sql, new Object[]{orderId}, (rs, rowNum) -> {
            OrderItemDTO item = new OrderItemDTO();
            item.setId(rs.getString("id"));
            item.setOrderId(rs.getString("order_id"));
            item.setProductId(rs.getString("product_id"));
            item.setProductName(rs.getString("product_name"));
            item.setQuantity(rs.getInt("quantity"));
            item.setUnitPrice(rs.getBigDecimal("unit_price"));
            item.setSubtotal(rs.getBigDecimal("subtotal"));
            return item;
        });
    }
    
    public List<OrderDTO> findByCustomerId(String customerId) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at " +
                "FROM orders WHERE customer_id = ? ORDER BY created_at DESC";
        
        return jdbcTemplate.query(sql, new Object[]{customerId}, (rs, rowNum) -> {
            OrderDTO order = new OrderDTO();
            order.setId(rs.getString("id"));
            order.setCustomerId(rs.getString("customer_id"));
            order.setStatus(rs.getString("status"));
            order.setTotalAmount(rs.getBigDecimal("total_amount"));
            order.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return order;
        });
    }
}

3.2.3 メリットとデメリット

メリット:

  • 単純で理解しやすい
  • 直接的なデータベースアクセス
  • 単純なCRUD操作に適している
  • パフォーマンスの最適化が容易

デメリット:

  • ビジネスロジックの重複
  • テスト容易性の低下
  • 複雑なビジネスルールの表現が難しい
  • コードの再利用性の低下

3.3 CQRS (Command Query Responsibility Segregation)

CQRSは、データの更新(コマンド)と読み取り(クエリ)の責任を分離するパターンです。

3.3.1 基本概念

  • コマンド: システムの状態を変更する操作
  • クエリ: システムの状態を読み取る操作
  • コマンドモデル: 更新操作に最適化されたモデル
  • クエリモデル: 読み取り操作に最適化されたモデル
  • イベントソーシング: 状態の変更をイベントとして記録

3.3.2 実装例 (Java)

// コマンド
public class CreateOrderCommand {
    private final String customerId;
    private final List<OrderItemRequest> items;
    
    public CreateOrderCommand(String customerId, List<OrderItemRequest> items) {
        this.customerId = customerId;
        this.items = items;
    }
    
    // getters
}

// コマンドハンドラ
@Component
public class CreateOrderCommandHandler {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final EventPublisher eventPublisher;
    
    public CreateOrderCommandHandler(
            OrderRepository orderRepository,
            ProductRepository productRepository,
            EventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.eventPublisher = eventPublisher;
    }
    
    public String handle(CreateOrderCommand command) {
        // 注文の作成
        Order order = new Order(
                new OrderId(UUID.randomUUID().toString()),
                new CustomerId(command.getCustomerId())
        );
        
        // 注文明細の追加
        for (OrderItemRequest itemRequest : command.getItems()) {
            Product product = productRepository.findById(new ProductId(itemRequest.getProductId()));
            order.addItem(product, itemRequest.getQuantity());
        }
        
        // 注文の確定
        order.confirm();
        
        // 注文の保存
        orderRepository.save(order);
        
        // イベントの発行
        eventPublisher.publish(new ```java
        // イベントの発行
        eventPublisher.publish(new OrderCreatedEvent(order));
        
        return order.getId().getValue();
    }
}

// クエリ
public class GetOrderQuery {
    private final String orderId;
    
    public GetOrderQuery(String orderId) {
        this.orderId = orderId;
    }
    
    // getters
}

// クエリハンドラ
@Component
public class GetOrderQueryHandler {
    private final OrderQueryRepository orderQueryRepository;
    
    public GetOrderQueryHandler(OrderQueryRepository orderQueryRepository) {
        this.orderQueryRepository = orderQueryRepository;
    }
    
    public OrderDTO handle(GetOrderQuery query) {
        return orderQueryRepository.findById(query.getOrderId());
    }
}

// クエリリポジトリ
@Repository
public class OrderQueryRepository {
    private final JdbcTemplate jdbcTemplate;
    
    @Autowired
    public OrderQueryRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public OrderDTO findById(String id) {
        String sql = "SELECT o.id, o.customer_id, c.name as customer_name, o.status, o.total_amount, o.created_at " +
                "FROM orders o " +
                "JOIN customers c ON o.customer_id = c.id " +
                "WHERE o.id = ?";
        
        OrderDTO order = jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
            OrderDTO dto = new OrderDTO();
            dto.setId(rs.getString("id"));
            dto.setCustomerId(rs.getString("customer_id"));
            dto.setCustomerName(rs.getString("customer_name"));
            dto.setStatus(rs.getString("status"));
            dto.setTotalAmount(rs.getBigDecimal("total_amount"));
            dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return dto;
        });
        
        if (order != null) {
            String itemsSql = "SELECT oi.id, oi.product_id, p.name as product_name, oi.quantity, oi.unit_price, oi.subtotal " +
                    "FROM order_items oi " +
                    "JOIN products p ON oi.product_id = p.id " +
                    "WHERE oi.order_id = ?";
            
            List<OrderItemDTO> items = jdbcTemplate.query(itemsSql, new Object[]{id}, (rs, rowNum) -> {
                OrderItemDTO item = new OrderItemDTO();
                item.setId(rs.getString("id"));
                item.setProductId(rs.getString("product_id"));
                item.setProductName(rs.getString("product_name"));
                item.setQuantity(rs.getInt("quantity"));
                item.setUnitPrice(rs.getBigDecimal("unit_price"));
                item.setSubtotal(rs.getBigDecimal("subtotal"));
                return item;
            });
            
            order.setItems(items);
        }
        
        return order;
    }
    
    public List<OrderSummaryDTO> findByCustomerId(String customerId) {
        String sql = "SELECT o.id, o.status, o.total_amount, o.created_at, COUNT(oi.id) as item_count " +
                "FROM orders o " +
                "LEFT JOIN order_items oi ON o.id = oi.order_id " +
                "WHERE o.customer_id = ? " +
                "GROUP BY o.id, o.status, o.total_amount, o.created_at " +
                "ORDER BY o.created_at DESC";
        
        return jdbcTemplate.query(sql, new Object[]{customerId}, (rs, rowNum) -> {
            OrderSummaryDTO summary = new OrderSummaryDTO();
            summary.setId(rs.getString("id"));
            summary.setStatus(rs.getString("status"));
            summary.setTotalAmount(rs.getBigDecimal("total_amount"));
            summary.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            summary.setItemCount(rs.getInt("item_count"));
            return summary;
        });
    }
}

// コントローラー
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final CommandBus commandBus;
    private final QueryBus queryBus;
    
    public OrderController(CommandBus commandBus, QueryBus queryBus) {
        this.commandBus = commandBus;
        this.queryBus = queryBus;
    }
    
    @PostMapping
    public ResponseEntity<String> createOrder(@RequestBody CreateOrderRequestDTO requestDTO) {
        CreateOrderCommand command = new CreateOrderCommand(
                requestDTO.getCustomerId(),
                requestDTO.getItems()
        );
        
        String orderId = commandBus.dispatch(command);
        
        return ResponseEntity.ok(orderId);
    }
    
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDTO> getOrder(@PathVariable String orderId) {
        GetOrderQuery query = new GetOrderQuery(orderId);
        
        OrderDTO order = queryBus.dispatch(query);
        
        return ResponseEntity.ok(order);
    }
    
    @GetMapping("/customer/{customerId}")
    public ResponseEntity<List<OrderSummaryDTO>> getCustomerOrders(@PathVariable String customerId) {
        GetCustomerOrdersQuery query = new GetCustomerOrdersQuery(customerId);
        
        List<OrderSummaryDTO> orders = queryBus.dispatch(query);
        
        return ResponseEntity.ok(orders);
    }
}

3.3.3 メリットとデメリット

メリット:

  • 読み取りと更新の最適化
  • スケーラビリティの向上
  • 複雑なドメインモデルと単純なクエリモデルの共存
  • パフォーマンスの向上

デメリット:

  • 複雑さの増加
  • データの一貫性の管理
  • 学習曲線が急
  • 小規模なアプリケーションでは過剰な場合がある

3.4 イベントソーシング

イベントソーシングは、システムの状態変更をイベントとして記録し、それらのイベントを再生することで現在の状態を再構築するパターンです。

3.4.1 基本概念

  • イベント: システムの状態変更を表す不変のレコード
  • イベントストア: イベントを永続化するためのストレージ
  • 集約: イベントを適用して状態を管理するドメインオブジェクト
  • スナップショット: パフォーマンス向上のための状態のキャプチャ
  • プロジェクション: 読み取り最適化のためのビュー

3.4.2 実装例 (Java)

// イベント
public interface DomainEvent {
    String getAggregateId();
    LocalDateTime getOccurredAt();
}

public class OrderCreatedEvent implements DomainEvent {
    private final String orderId;
    private final String customerId;
    private final LocalDateTime occurredAt;
    
    public OrderCreatedEvent(String orderId, String customerId) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.occurredAt = LocalDateTime.now();
    }
    
    @Override
    public String getAggregateId() {
        return orderId;
    }
    
    @Override
    public LocalDateTime getOccurredAt() {
        return occurredAt;
    }
    
    // getters
}

public class OrderItemAddedEvent implements DomainEvent {
    private final String orderId;
    private final String productId;
    private final int quantity;
    private final BigDecimal unitPrice;
    private final LocalDateTime occurredAt;
    
    public OrderItemAddedEvent(String orderId, String productId, int quantity, BigDecimal unitPrice) {
        this.orderId = orderId;
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
        this.occurredAt = LocalDateTime.now();
    }
    
    @Override
    public String getAggregateId() {
        return orderId;
    }
    
    @Override
    public LocalDateTime getOccurredAt() {
        return occurredAt;
    }
    
    // getters
}

public class OrderConfirmedEvent implements DomainEvent {
    private final String orderId;
    private final LocalDateTime occurredAt;
    
    public OrderConfirmedEvent(String orderId) {
        this.orderId = orderId;
        this.occurredAt = LocalDateTime.now();
    }
    
    @Override
    public String getAggregateId() {
        return orderId;
    }
    
    @Override
    public LocalDateTime getOccurredAt() {
        return occurredAt;
    }
    
    // getters
}

// 集約
public class Order {
    private String id;
    private String customerId;
    private List<OrderItem> items = new ArrayList<>();
    private String status;
    private BigDecimal totalAmount = BigDecimal.ZERO;
    private LocalDateTime createdAt;
    
    // イベントソーシング用のコンストラクタ
    public Order(List<DomainEvent> events) {
        if (events.isEmpty()) {
            throw new IllegalArgumentException("Events cannot be empty");
        }
        
        for (DomainEvent event : events) {
            apply(event);
        }
    }
    
    // コマンド処理メソッド
    public List<DomainEvent> process(CreateOrderCommand command) {
        if (id != null) {
            throw new IllegalStateException("Order already exists");
        }
        
        String orderId = UUID.randomUUID().toString();
        
        List<DomainEvent> events = new ArrayList<>();
        events.add(new OrderCreatedEvent(orderId, command.getCustomerId()));
        
        return events;
    }
    
    public List<DomainEvent> process(AddOrderItemCommand command) {
        if (!"DRAFT".equals(status)) {
            throw new IllegalStateException("Cannot add items to a non-draft order");
        }
        
        List<DomainEvent> events = new ArrayList<>();
        events.add(new OrderItemAddedEvent(
                id,
                command.getProductId(),
                command.getQuantity(),
                command.getUnitPrice()
        ));
        
        return events;
    }
    
    public List<DomainEvent> process(ConfirmOrderCommand command) {
        if (!"DRAFT".equals(status)) {
            throw new IllegalStateException("Cannot confirm a non-draft order");
        }
        
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm an empty order");
        }
        
        List<DomainEvent> events = new ArrayList<>();
        events.add(new OrderConfirmedEvent(id));
        
        return events;
    }
    
    // イベント適用メソッド
    private void apply(DomainEvent event) {
        if (event instanceof OrderCreatedEvent) {
            apply((OrderCreatedEvent) event);
        } else if (event instanceof OrderItemAddedEvent) {
            apply((OrderItemAddedEvent) event);
        } else if (event instanceof OrderConfirmedEvent) {
            apply((OrderConfirmedEvent) event);
        }
    }
    
    private void apply(OrderCreatedEvent event) {
        this.id = event.getAggregateId();
        this.customerId = event.getCustomerId();
        this.status = "DRAFT";
        this.createdAt = event.getOccurredAt();
    }
    
    private void apply(OrderItemAddedEvent event) {
        OrderItem item = new OrderItem(
                event.getProductId(),
                event.getQuantity(),
                event.getUnitPrice()
        );
        
        items.add(item);
        
        // 合計金額の再計算
        recalculateTotal();
    }
    
    private void apply(OrderConfirmedEvent event) {
        this.status = "CONFIRMED";
    }
    
    private void recalculateTotal() {
        this.totalAmount = items.stream()
                .map(item -> item.getUnitPrice().multiply(new BigDecimal(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // getters
}

// イベントストア
@Repository
public class EventStore {
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    
    @Autowired
    public EventStore(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }
    
    public void saveEvents(String aggregateId, List<DomainEvent> events, int expectedVersion) {
        // 楽観的ロックのためのバージョンチェック
        int currentVersion = getAggregateVersion(aggregateId);
        if (currentVersion != expectedVersion) {
            throw new ConcurrencyException("Aggregate version mismatch");
        }
        
        // イベントの保存
        for (DomainEvent event : events) {
            saveEvent(event, ++currentVersion);
        }
    }
    
    private void saveEvent(DomainEvent event, int version) {
        try {
            String eventType = event.getClass().getSimpleName();
            String payload = objectMapper.writeValueAsString(event);
            
            String sql = "INSERT INTO events (aggregate_id, version, event_type, payload, occurred_at) " +
                    "VALUES (?, ?, ?, ?, ?)";
            
            jdbcTemplate.update(sql,
                    event.getAggregateId(),
                    version,
                    eventType,
                    payload,
                    Timestamp.valueOf(event.getOccurredAt())
            );
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to serialize event", e);
        }
    }
    
    public List<DomainEvent> getEvents(String aggregateId) {
        String sql = "SELECT event_type, payload FROM events " +
                "WHERE aggregate_id = ? ORDER BY version ASC";
        
        return jdbcTemplate.query(sql, new Object[]{aggregateId}, (rs, rowNum) -> {
            String eventType = rs.getString("event_type");
            String payload = rs.getString("payload");
            
            try {
                Class<?> eventClass = Class.forName("com.example.domain.events." + eventType);
                return (DomainEvent) objectMapper.readValue(payload, eventClass);
            } catch (Exception e) {
                throw new RuntimeException("Failed to deserialize event", e);
            }
        });
    }
    
    private int getAggregateVersion(String aggregateId) {
        String sql = "SELECT MAX(version) FROM events WHERE aggregate_id = ?";
        
        Integer version = jdbcTemplate.queryForObject(sql, new Object[]{aggregateId}, Integer.class);
        
        return version != null ? version : 0;
    }
}

// リポジトリ
@Repository
public class OrderRepository {
    private final EventStore eventStore;
    
    @Autowired
    public OrderRepository(EventStore eventStore) {
        this.eventStore = eventStore;
    }
    
    public Order findById(String orderId) {
        List<DomainEvent> events = eventStore.getEvents(orderId);
        
        if (events.isEmpty()) {
            return null;
        }
        
        return new Order(events);
    }
    
    public void save(Order order, List<DomainEvent> newEvents, int expectedVersion) {
        eventStore.saveEvents(order.getId(), newEvents, expectedVersion);
    }
}

// コマンドハンドラ
@Component
public class OrderCommandHandler {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    @Autowired
    public OrderCommandHandler(OrderRepository orderRepository, ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }
    
    public String handle(CreateOrderCommand command) {
        // 新しい注文の作成
        Order order = new Order(Collections.emptyList());
        List<DomainEvent> events = order.process(command);
        
        // イベントの保存
        orderRepository.save(order, events, 0);
        
        // 注文IDの返却
        return ((OrderCreatedEvent) events.get(0)).getAggregateId();
    }
    
    public void handle(AddOrderItemCommand command) {
        // 注文の取得
        Order order = orderRepository.findById(command.getOrderId());
        if (order == null) {
            throw new EntityNotFoundException("Order not found");
        }
        
        // 商品の取得
        Product product = productRepository.findById(command.getProductId());
        if (product == null) {
            throw new EntityNotFoundException("Product not found");
        }
        
        // コマンドの処理
        AddOrderItemCommand enrichedCommand = new AddOrderItemCommand(
                command.getOrderId(),
                command.getProductId(),
                command.getQuantity(),
                product.getPrice()
        );
        
        List<DomainEvent> events = order.process(enrichedCommand);
        
        // イベントの保存
        orderRepository.save(order, events, getVersion(order));
    }
    
    public void handle(ConfirmOrderCommand command) {
        // 注文の取得
        Order order = orderRepository.findById(command.getOrderId());
        if (order == null) {
            throw new EntityNotFoundException("Order not found");
        }
        
        // コマンドの処理
        List<DomainEvent> events = order.process(command);
        
        // イベントの保存
        orderRepository.save(order, events, getVersion(order));
    }
    
    private int getVersion(Order order) {
        // 実際の実装では、集約からバージョンを取得する方法が必要
        // ここでは簡略化のため、0を返す
        return 0;
    }
}

// プロジェクション
@Component
public class OrderProjection {
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    
    @Autowired
    public OrderProjection(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.jdbcTemplate = jdbcTemplate;
        this.objectMapper = objectMapper;
    }
    
    @EventListener
    public void on(OrderCreatedEvent event) {
        String sql = "INSERT INTO order_view (id, customer_id, status, total_amount, created_at) " +
                "VALUES (?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(sql,
                event.getAggregateId(),
                event.getCustomerId(),
                "DRAFT",
                BigDecimal.ZERO,
                Timestamp.valueOf(event.getOccurredAt())
        );
    }
    
    @EventListener
    public void on(OrderItemAddedEvent event) {
        // 注文明細の追加
        String itemSql = "INSERT INTO order_item_view (order_id, product_id, quantity, unit_price) " +
                "VALUES (?, ?, ?, ?)";
        
        jdbcTemplate.update(itemSql,
                event.getAggregateId(),
                event.getProductId(),
                event.getQuantity(),
                event.getUnitPrice()
        );
        
        // 注文の合計金額の更新
        String updateSql = "UPDATE order_view SET total_amount = " +
                "(SELECT SUM(quantity * unit_price) FROM order_item_view WHERE order_id = ?) " +
                "WHERE id = ?";
        
        jdbcTemplate.update(updateSql,
                event.getAggregateId(),
                event.getAggregateId()
        );
    }
    
    @EventListener
    public void on(OrderConfirmedEvent event) {
        String sql = "UPDATE order_view SET status = ? WHERE id = ?";
        
        jdbcTemplate.update(sql,
                "CONFIRMED",
                event.getAggregateId()
        );
    }
}

3.4.3 メリットとデメリット

メリット:

  • 完全な監査証跡
  • 時間を遡った状態の再構築
  • ドメインイベントの自然な表現
  • 並行処理の容易さ

デメリット:

  • 学習曲線が急
  • クエリのパフォーマンス課題
  • イベントスキーマの進化の管理
  • 実装の複雑さ

4. データアクセスパターン

4.1 リポジトリパターン

リポジトリパターンは、データアクセスロジックをカプセル化し、コレクションのようなインターフェースを提供するパターンです。

4.1.1 基本概念

  • リポジトリインターフェース: ドメインオブジェクトの永続化操作を定義
  • リポジトリ実装: データアクセス技術に依存する実装
  • 仕様 (Specification): クエリ条件をオブジェクトとして表現

4.1.2 実装例 (Java)

// リポジトリインターフェース
public interface OrderRepository {
    Order findById(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
    List<Order> findByStatus(OrderStatus status);
    List<Order> findBySpecification(Specification<Order> specification);
    void save(Order order);
    void delete(Order order);
}

// 仕様インターフェース
public interface Specification<T> {
    boolean isSatisfiedBy(T entity);
    String toSqlCriteria();
    Map<String, Object> getParameters();
}

// 仕様の実装例
public class OrderByDateRangeSpecification implements Specification<Order> {
    private final LocalDateTime startDate;
    private final LocalDateTime endDate;
    
    public OrderByDateRangeSpecification(LocalDateTime startDate, LocalDateTime endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }
    
    @Override
    public boolean isSatisfiedBy(Order order) {
        LocalDateTime orderDate = order.getCreatedAt();
        return !orderDate.isBefore(startDate) && !orderDate.isAfter(endDate);
    }
    
    @Override
    public String toSqlCriteria() {
        return "created_at >= :startDate AND created_at <= :endDate";
    }
    
    @Override
    public Map<String, Object> getParameters() {
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("startDate", startDate);
        parameters.put("endDate", endDate);
        return parameters;
    }
}

// JPA実装
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final EntityManager entityManager;
    
    @Autowired
    public JpaOrderRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    
    @Override
    public Order findById(OrderId id) {
        return entityManager.find(Order.class, id);
    }
    
    @Override
    public List<Order> findByCustomerId(CustomerId customerId) {
        TypedQuery<Order> query = entityManager.createQuery(
                "SELECT o FROM Order o WHERE o.customerId = :customerId ORDER BY o.createdAt DESC",
                Order.class
        );
        query.setParameter("customerId", customerId);
        return query.getResultList();
    }
    
    @Override
    public List<Order> findByStatus(OrderStatus status) {
        TypedQuery<Order> query = entityManager.createQuery(
                "SELECT o FROM Order o WHERE o.status = :status",
                Order.class
        );
        query.setParameter("status", status);
        return query.getResultList();
    }
    
    @Override
    public List<Order> findBySpecification(Specification<Order> specification) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> root = cq.from(Order.class);
        cq.select(root);
        
        // 仕様からCriteriaQueryを構築
        // 実際の実装では、仕様からCriteriaQueryへの変換ロジックが必要
        // ここでは簡略化のため、JPQLを使用
        
        String jpql = "SELECT o FROM Order o WHERE " + specification.toSqlCriteria();
        TypedQuery<Order> query = entityManager.createQuery(jpql, Order.class);
        
        // パラメータの設定
        for (Map.Entry<String, Object> entry : specification.getParameters().entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        
        return query.getResultList();
    }
    
    @Override
    public void save(Order order) {
        if (entityManager.contains(order)) {
            entityManager.merge(order);
        } else {
            entityManager.persist(order);
        }
    }
    
    @Override
    public void delete(Order order) {
        entityManager.remove(order);
    }
}

4.1.3 メリットとデメリット

メリット:

  • データアクセスロジックの分離
  • ドメインモデルの永続化の詳細を隠蔽
  • テスト容易性の向上
  • 柔軟なクエリ構築

デメリット:

  • リポジトリインターフェースの肥大化
  • 複雑なクエリの表現が難しい場合がある
  • パフォーマンスの最適化が難しい場合がある

4.2 DAO (Data Access Object) パターン

DAOパターンは、データアクセスロジックをカプセル化し、特定のデータソースへのアクセスを抽象化するパターンです。

4.2.1 基本概念

  • DAOインターフェース: データアクセス操作を定義
  • DAO実装: 特定のデータアクセス技術に依存する実装
  • データ転送オブジェクト (DTO): レイヤー間のデータ転送に使用

4.2.2 実装例 (Java)

// DAOインターフェース
public interface OrderDAO {
    OrderDTO findById(String id);
    List<OrderDTO> findByCustomerId(String customerId);
    List<OrderDTO> findByStatus(String status);
    List<OrderDTO> findByDateRange(LocalDateTime startDate, LocalDateTime endDate);
    void save(OrderDTO order);
    void update(OrderDTO order);
    void delete(String id);
}

// DTO
public class OrderDTO {
    private String id;
    private String customerId;
    private String status;
    private BigDecimal totalAmount;
    private LocalDateTime createdAt;
    private List<OrderItemDTO> items;
    
    // getters and setters
}

public class OrderItemDTO {
    private String id;
    private String orderId;
    private String productId;
    private String productName;
    private int quantity;
    private BigDecimal unitPrice;
    private BigDecimal subtotal;
    
    // getters and setters
}

// JDBC実装
@Repository
public class JdbcOrderDAO implements OrderDAO {
    private final JdbcTemplate jdbcTemplate;
    
    @Autowired
    public JdbcOrderDAO(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    @Override
    public OrderDTO findById(String id) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at FROM orders WHERE id = ?";
        
        try {
            OrderDTO order = jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
                OrderDTO dto = new OrderDTO();
                dto.setId(rs.getString("id"));
                dto.setCustomerId(rs.getString("customer_id"));
                dto.setStatus(rs.getString("status"));
                dto.setTotalAmount(rs.getBigDecimal("total_amount"));
                dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
                return dto;
            });
            
            if (order != null) {
                order.setItems(findOrderItems(id));
            }
            
            return order;
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }
    
    private List<OrderItemDTO> findOrderItems(String orderId) {
        String sql = "SELECT id, order_id, product_id, product_name, quantity, unit_price, unit_price * quantity as subtotal " +
                "FROM order_items WHERE order_id = ?";
        
        return jdbcTemplate.query(sql, new Object[]{orderId}, (rs, rowNum) -> {
            OrderItemDTO item = new OrderItemDTO();
            item.setId(rs.getString("id"));
            item.setOrderId(rs.getString("order_id"));
            item.setProductId(rs.getString("product_id"));
            item.setProductName(rs.getString("product_name"));
            item.setQuantity(rs.getInt("quantity"));
            item.setUnitPrice(rs.getBigDecimal("unit_price"));
            item.setSubtotal(rs.getBigDecimal("subtotal"));
            return item;
        });
    }
    
    @Override
    public List<OrderDTO> findByCustomerId(String customerId) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at " +
                "FROM orders WHERE customer_id = ? ORDER BY created_at DESC";
        
        List<OrderDTO> orders = jdbcTemplate.query(sql, new Object[]{customerId}, (rs, rowNum) -> {
            OrderDTO dto = new OrderDTO();
            dto.setId(rs.getString("id"));
            dto.setCustomerId(rs.getString("customer_id"));
            dto.setStatus(rs.getString("status"));
            dto.setTotalAmount(rs.getBigDecimal("total_amount"));
            dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return dto;
        });
        
        for (OrderDTO order : orders) {
            order.setItems(findOrderItems(order.getId()));
        }
        
        return orders;
    }
    
    @Override
    public List<OrderDTO> findByStatus(String status) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at " +
                "FROM orders WHERE status = ?";
        
        List<OrderDTO> orders = jdbcTemplate.query(sql, new Object[]{status}, (rs, rowNum) -> {
            OrderDTO dto = new OrderDTO();
            dto.setId(rs.getString("id"));
            dto.setCustomerId(rs.getString("customer_id"));
            dto.setStatus(rs.getString("status"));
            dto.setTotalAmount(rs.getBigDecimal("total_amount"));
            dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return dto;
        });
        
        for (OrderDTO order : orders) {
            order.setItems(findOrderItems(order.getId()));
        }
        
        return orders;
    }
    
    @Override
    public List<OrderDTO> findByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
        String sql = "SELECT id, customer_id, status, total_amount, created_at " +
                "FROM orders WHERE created_at >= ? AND created_at <= ?";
        
        List<OrderDTO> orders = jdbcTemplate.query(sql, new Object[]{
                Timestamp.valueOf(startDate),
                Timestamp.valueOf(endDate)
        }, (rs, rowNum) -> {
            OrderDTO dto = new OrderDTO();
            dto.setId(rs.getString("id"));
            dto.setCustomerId(rs.getString("customer_id"));
            dto.setStatus(rs.getString("status"));
            dto.setTotalAmount(rs.getBigDecimal("total_amount"));
            dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return dto;
        });
        
        for (OrderDTO order : orders) {
            order.setItems(findOrderItems(order.getId()));
        }
        
        return orders;
    }
    
    @Override
    public void save(OrderDTO order) {
        // 注文の保存
        String orderSql = "INSERT INTO orders (id, customer_id, status, total_amount, created_at) " +
                "VALUES (?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(orderSql,
                order.getId(),
                order.getCustomerId(),
                order.getStatus(),
                order.getTotalAmount(),
                Timestamp.valueOf(order.getCreatedAt())
        );
        
        // 注文明細の保存
        if (order.getItems() != null) {
            for (OrderItemDTO item : order.getItems()) {
                String itemSql = "INSERT INTO order_items (id, order_id, product_id, product_name, quantity, unit_price) " +
                        "VALUES (?, ?, ?, ?, ?, ?)";
                
                jdbcTemplate.update(itemSql,
                        item.getId(),
                        order.getId(),
                        item.getProductId(),
                        item.getProductName(),
                        item.getQuantity(),
                        item.getUnitPrice()
                );
            }
        }
    }
    
    @Override
    public void update(OrderDTO order) {
        // 注文の更新
        String orderSql = "UPDATE orders SET customer_id = ?, status = ?, total_amount = ? WHERE id = ?";
        
        jdbcTemplate.update(orderSql,
                order.getCustomerId(),
                order.getStatus(),
                order.getTotalAmount(),
                order.getId()
        );
        
        // 注文明細の更新(既存の明細を削除して再作成)
        String deleteItemsSql = "DELETE FROM order_items WHERE order_id = ?";
        jdbcTemplate.update(deleteItemsSql, order.getId());
        
        if (order.getItems() != null) {
            for (OrderItemDTO item : order.getItems()) {
                String itemSql = "INSERT INTO order_items (id, order_id, product_id, product_name, quantity, unit_price) " +
                        "VALUES (?, ?, ?, ?, ?, ?)";
                
                jdbcTemplate.update(itemSql,
                        item.getId(),
                        order.getId(),
                        item.getProductId(),
                        item.getProductName(),
                        item.getQuantity(),
                        item.getUnitPrice()
                );
            }
        }
    }
    
    @Override
    public void delete(String id) {
        // 注文明細の削除
        String deleteItemsSql = "DELETE FROM order_items WHERE order_id = ?";
        jdbcTemplate.update(deleteItemsSql, id);
        
        // 注文の削除
        String deleteOrderSql = "DELETE FROM orders WHERE id = ?";
        jdbcTemplate.update(deleteOrderSql, id);
    }
}

4.2.3 メリットとデメリット

メリット:

  • データアクセスロジックの分離
  • 特定のデータアクセス技術への依存の局所化
  • シンプルで理解しやすい
  • 直接的なSQLの最適化が容易

デメリット:

  • ドメインモデルとの分離による変換オーバーヘッド
  • DTOの管理コスト
  • ビジネスロジックの漏洩リスク

4.3 ORM (Object-Relational Mapping)

ORMは、オブジェクト指向モデルとリレーショナルデータベースの間のマッピングを自動化する技術です。

4.3.1 基本概念

  • エンティティマッピング: クラスとテーブルのマッピング
  • 関連マッピング: オブジェクト間の関連とテーブル間の関連のマッピング
  • 継承マッピング: クラス階層とテーブルのマッピング
  • クエリ言語: オブジェクト指向クエリ言語(JPQL, HQLなど)

4.3.2 実装例 (Java JPA)

// エンティティ
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @Column(name = "id")
    private String id;
    
    @Column(name = "customer_id", nullable = false)
    private String customerId;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private OrderStatus status;
    
    @Column(name = "total_amount", precision = 10, scale = 2)
    private BigDecimal totalAmount;
    
    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
    
    // ビジネスロジック
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem();
        item.setOrder(this);
        item.setProductId(product.getId());
        item.setProductName(product.getName());
        item.setQuantity(quantity);
        item.setUnitPrice(product.getPrice());
        
        items.add(item);
        recalculateTotal();
    }
    
    public void removeItem(String itemId) {
        items.removeIf(item -> item.getId().equals(itemId));
        recalculateTotal();
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
                .map(item -> item.getUnitPrice().multiply(new BigDecimal(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // getters and setters
}

@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    @Column(name = "id")
    private String id;
    
    @ManyToOne
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;
    
    @Column(name = "product_id", nullable = false)
    private String productId;
    
    @Column(name = "product_name", nullable = false)
    private String productName;
    
    @Column(name = "quantity", nullable = false)
    private int quantity;
    
    @Column(name = "unit_price", precision = 10, scale = 2, nullable = false)
    private BigDecimal unitPrice;
    
    // getters and setters
}

// リポジトリ
@Repository
public interface OrderRepository extends JpaRepository<Order, String> {
    List<Order> findByCustomerId(String customerId);
    List<Order> findByStatus(OrderStatus status);
    List<Order> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
    
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") String id);
}

// サービス
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    @Autowired
    public OrderService(OrderRepository orderRepository, ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }
    
    @Transactional
    public Order createOrder(String customerId, List<OrderItemRequest> itemRequests) {
        Order order = new Order();
        order.setId(UUID.randomUUID().toString());
        order.setCustomerId(customerId);
        order.setStatus(OrderStatus.DRAFT);
        order.setCreatedAt(LocalDateTime.now());
        
        for (OrderItemRequest itemRequest : itemRequests) {
            Product product = productRepository.findById(itemRequest.getProductId())
                    .orElseThrow(() -> new EntityNotFoundException("Product not found"));
            
            order.addItem(product, itemRequest.getQuantity());
        }
        
        order.setStatus(OrderStatus.CONFIRMED);
        
        return orderRepository.save(order);
    }
    
    @Transactional(readOnly = true)
    public Order getOrder(String orderId) {
        return orderRepository.findByIdWithItems(orderId)
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
    }
    
    @Transactional(readOnly = true)
    public List<Order> getOrdersByCustomerId(String customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
    
    @Transactional
    public void cancelOrder(String orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
        
        if (order.getStatus() == OrderStatus.SHIPPED || order.getStatus() == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel a shipped or delivered order");
        }
        
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

4.3.3 メリットとデメリット

メリット:

  • オブジェクト指向モデルとリレーショナルデータベースの統合
  • ボイラープレートコードの削減
  • データベース独立性
  • キャッシングなどの最適化

デメリット:

  • 学習曲線が急
  • 複雑なクエリのパフォーマンス問題
  • N+1クエリ問題
  • 過度の抽象化による制御の喪失

5. プレゼンテーション層のパターン

5.1 MVC (Model-View-Controller)

MVCは、アプリケーションを3つの主要なコンポーネントに分割するアーキテクチャパターンです。

5.1.1 基本概念

  • モデル: データとビジネスロジック
  • ビュー: ユーザーインターフェース
  • コントローラー: ユーザー入力の処理とモデルとビューの調整

5.1.2 実装例 (Spring MVC)

// モデル
public class OrderViewModel {
    private String id;
    private String customerName;
    private String status;
    private BigDecimal totalAmount;
    private LocalDateTime createdAt;
    private List<OrderItemViewModel> items;
    
    // getters and setters
}

public class OrderItemViewModel {
    private String productName;
    private int quantity;
    private BigDecimal unitPrice;
    private BigDecimal subtotal;
    
    // getters and setters
}

// コントローラー
@Controller
@RequestMapping("/orders")
public class OrderController {
    private final OrderService orderService;
    private final CustomerService customerService;
    
    @Autowired
    public OrderController(OrderService orderService, CustomerService customerService) {
        this.orderService = orderService;
        this.customerService = customerService;
    }
    
    @GetMapping
    public String listOrders(Model model) {
        List<Order> orders = orderService.getAllOrders();
        
        List<OrderViewModel> viewModels = orders.stream()
                .map(this::convertToViewModel)
                .collect(Collectors.toList());
        
        model.addAttribute("orders", viewModels);
        
        return "order/list";
    }
    
    @GetMapping("/{orderId}")
    public String viewOrder(@PathVariable String orderId, Model model) {
        Order order = orderService.getOrder(orderId);
        OrderViewModel viewModel = convertToViewModel(order);
        
        model.addAttribute("order", viewModel);
        
        return "order/view";
    }
    
    @GetMapping("/create")
    public String createOrderForm(Model model) {
        model.addAttribute("customers", customerService.getAllCustomers());
        model.addAttribute("products", productService.getAllProducts());
        
        return "order/create";
    }
    
    @PostMapping("/create")
    public String createOrder(@ModelAttribute CreateOrderRequest request, RedirectAttributes redirectAttributes) {
        try {
            Order order = orderService.createOrder(request.getCustomerId(), request.getItems());
            redirectAttributes.addFlashAttribute("successMessage", "注文が正常に作成されました。");
            return "redirect:/orders/" + order.getId();
        } catch (Exception e) {
            redirectAttributes.addFlashAttribute("errorMessage", "注文の作成に失敗しました: " + e.getMessage());
            return "redirect:/orders/create";
        }
    }
    
    private OrderViewModel convertToViewModel(Order order) {
        OrderViewModel viewModel = new OrderViewModel();
        viewModel.setId(order.getId());
        
        // 顧客名の取得
        Customer customer = customerService.getCustomer(order.getCustomerId());
        viewModel.setCustomerName(customer.getName());
        
        viewModel.setStatus(order.getStatus().getDisplayName());
        viewModel.setTotalAmount(order.getTotalAmount());
        viewModel.setCreatedAt(order.getCreatedAt());
        
        // 注文明細の変換
        List<OrderItemViewModel> itemViewModels = order.getItems().stream()
                .map(item -> {
                    OrderItemViewModel itemViewModel = new OrderItemViewModel();
                    itemViewModel.setProductName(item.getProductName());
                    itemViewModel.setQuantity(item.getQuantity());
                    itemViewModel.setUnitPrice(item.getUnitPrice());
                    itemViewModel.setSubtotal(item.getUnitPrice().multiply(new BigDecimal(item.getQuantity())));
                    return itemViewModel;
                })
                .collect(Collectors.toList());
        
        viewModel.setItems(itemViewModels);
        
        return viewModel;
    }
}

5.1.3 メリットとデメリット

メリット:

  • メリット:
  • 関心の分離
  • 並行開発の容易化
  • コンポーネントの再利用性
  • テスト容易性の向上

デメリット:

  • コントローラーの肥大化
  • ビューとモデルの密結合
  • 複雑なユーザーインターフェースでの管理の難しさ

5.2 MVVM (Model-View-ViewModel)

MVVMは、特にリッチクライアントアプリケーションのためのアーキテクチャパターンです。

5.2.1 基本概念

  • モデル: ビジネスロジックとデータ
  • ビュー: ユーザーインターフェース
  • ビューモデル: ビューのための状態とコマンド

5.2.2 実装例 (Vue.js)

// モデル(APIクライアント)
class OrderService {
  async getOrders() {
    const response = await fetch('/api/orders');
    return response.json();
  }
  
  async getOrder(orderId) {
    const response = await fetch(`/api/orders/${orderId}`);
    return response.json();
  }
  
  async createOrder(orderData) {
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(orderData)
    });
    return response.json();
  }
}

// ビューモデル(Vue.js)
const OrderListViewModel = {
  data() {
    return {
      orders: [],
      loading: false,
      error: null
    };
  },
  
  created() {
    this.fetchOrders();
  },
  
  methods: {
    async fetchOrders() {
      this.loading = true;
      this.error = null;
      
      try {
        const orderService = new OrderService();
        this.orders = await orderService.getOrders();
      } catch (error) {
        this.error = '注文の取得に失敗しました: ' + error.message;
      } finally {
        this.loading = false;
      }
    },
    
    formatDate(dateString) {
      return new Date(dateString).toLocaleString();
    },
    
    formatCurrency(amount) {
      return new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(amount);
    }
  }
};

// ビュー(Vue.js テンプレート)
`
<div>
  <h1>注文一覧</h1>
  
  <div v-if="loading" class="loading">
    読み込み中...
  </div>
  
  <div v-if="error" class="error">
    {{ error }}
  </div>
  
  <table v-if="!loading && !error && orders.length > 0" class="order-table">
    <thead>
      <tr>
        <th>注文番号</th>
        <th>顧客名</th>
        <th>ステータス</th>
        <th>合計金額</th>
        <th>注文日時</th>
        <th>アクション</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="order in orders" :key="order.id">
        <td>{{ order.id }}</td>
        <td>{{ order.customerName }}</td>
        <td>{{ order.status }}</td>
        <td>{{ formatCurrency(order.totalAmount) }}</td>
        <td>{{ formatDate(order.createdAt) }}</td>
        <td>
          <router-link :to="'/orders/' + order.id">詳細</router-link>
        </td>
      </tr>
    </tbody>
  </table>
  
  <div v-if="!loading && !error && orders.length === 0" class="empty-state">
    注文がありません。
  </div>
  
  <div class="actions">
    <router-link to="/orders/create" class="button">新規注文</router-link>
  </div>
</div>
`

5.2.3 メリットとデメリット

メリット:

  • データバインディングによる自動UI更新
  • ビューとロジックの分離
  • ユニットテストの容易さ
  • 状態管理の一元化

デメリット:

  • 学習曲線が急
  • 小規模なアプリケーションでは過剰な場合がある
  • パフォーマンスへの影響(大量のバインディング)

5.3 Flux/Redux

Flux/Reduxは、単方向データフローを強制するアーキテクチャパターンです。

5.3.1 基本概念

  • アクション: 状態変更の意図を表すオブジェクト
  • ディスパッチャー: アクションをストアに送信
  • ストア: アプリケーションの状態を保持
  • ビュー: ストアの状態に基づいてレンダリング

5.3.2 実装例 (React + Redux)

// アクション
const ActionTypes = {
  FETCH_ORDERS_REQUEST: 'FETCH_ORDERS_REQUEST',
  FETCH_ORDERS_SUCCESS: 'FETCH_ORDERS_SUCCESS',
  FETCH_ORDERS_FAILURE: 'FETCH_ORDERS_FAILURE',
  CREATE_ORDER_REQUEST: 'CREATE_ORDER_REQUEST',
  CREATE_ORDER_SUCCESS: 'CREATE_ORDER_SUCCESS',
  CREATE_ORDER_FAILURE: 'CREATE_ORDER_FAILURE'
};

// アクションクリエーター
const fetchOrders = () => async (dispatch) => {
  dispatch({ type: ActionTypes.FETCH_ORDERS_REQUEST });
  
  try {
    const response = await fetch('/api/orders');
    const orders = await response.json();
    
    dispatch({
      type: ActionTypes.FETCH_ORDERS_SUCCESS,
      payload: orders
    });
  } catch (error) {
    dispatch({
      type: ActionTypes.FETCH_ORDERS_FAILURE,
      payload: error.message
    });
  }
};

const createOrder = (orderData) => async (dispatch) => {
  dispatch({ type: ActionTypes.CREATE_ORDER_REQUEST });
  
  try {
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(orderData)
    });
    
    const order = await response.json();
    
    dispatch({
      type: ActionTypes.CREATE_ORDER_SUCCESS,
      payload: order
    });
    
    return order;
  } catch (error) {
    dispatch({
      type: ActionTypes.CREATE_ORDER_FAILURE,
      payload: error.message
    });
    
    throw error;
  }
};

// リデューサー
const initialState = {
  orders: [],
  loading: false,
  error: null,
  currentOrder: null
};

const orderReducer = (state = initialState, action) => {
  switch (action.type) {
    case ActionTypes.FETCH_ORDERS_REQUEST:
      return {
        ...state,
        loading: true,
        error: null
      };
    
    case ActionTypes.FETCH_ORDERS_SUCCESS:
      return {
        ...state,
        loading: false,
        orders: action.payload
      };
    
    case ActionTypes.FETCH_ORDERS_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    
    case ActionTypes.CREATE_ORDER_REQUEST:
      return {
        ...state,
        loading: true,
        error: null
      };
    
    case ActionTypes.CREATE_ORDER_SUCCESS:
      return {
        ...state,
        loading: false,
        orders: [...state.orders, action.payload],
        currentOrder: action.payload
      };
    
    case ActionTypes.CREATE_ORDER_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    
    default:
      return state;
  }
};

// コンポーネント
const OrderList = ({ orders, loading, error, fetchOrders }) => {
  useEffect(() => {
    fetchOrders();
  }, [fetchOrders]);
  
  if (loading) {
    return <div>読み込み中...</div>;
  }
  
  if (error) {
    return <div className="error">エラー: {error}</div>;
  }
  
  return (
    <div>
      <h1>注文一覧</h1>
      
      {orders.length === 0 ? (
        <div>注文がありません。</div>
      ) : (
        <table className="order-table">
          <thead>
            <tr>
              <th>注文番号</th>
              <th>顧客名</th>
              <th>ステータス</th>
              <th>合計金額</th>
              <th>注文日時</th>
              <th>アクション</th>
            </tr>
          </thead>
          <tbody>
            {orders.map(order => (
              <tr key={order.id}>
                <td>{order.id}</td>
                <td>{order.customerName}</td>
                <td>{order.status}</td>
                <td>{formatCurrency(order.totalAmount)}</td>
                <td>{formatDate(order.createdAt)}</td>
                <td>
                  <Link to={`/orders/${order.id}`}>詳細</Link>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      
      <div className="actions">
        <Link to="/orders/create" className="button">新規注文</Link>
      </div>
    </div>
  );
};

// Reduxとの接続
const mapStateToProps = (state) => ({
  orders: state.order.orders,
  loading: state.order.loading,
  error: state.order.error
});

const mapDispatchToProps = {
  fetchOrders
};

export default connect(mapStateToProps, mapDispatchToProps)(OrderList);

5.3.3 メリットとデメリット

メリット:

  • 予測可能な状態管理
  • デバッグの容易さ
  • 単方向データフロー
  • 時間旅行デバッグ

デメリット:

  • ボイラープレートコードの増加
  • 学習曲線が急
  • 小規模なアプリケーションでは過剰な場合がある
  • パフォーマンスへの影響(大規模アプリケーション)

6. 業務アプリケーションの横断的関心事

6.1 認証と認可

6.1.1 基本概念

  • 認証 (Authentication): ユーザーの身元確認
  • 認可 (Authorization): ユーザーのアクセス権限の確認
  • ロールベースアクセス制御 (RBAC): ロールに基づく権限管理
  • 属性ベースアクセス制御 (ABAC): 属性に基づく権限管理

6.1.2 実装例 (Spring Security)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home", "/register", "/css/**", "/js/**").permitAll()
                .antMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .logout()
                .permitAll()
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// カスタムユーザーサービス
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                true, true, true,
                getAuthorities(user.getRoles())
        );
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(Set<Role> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList());
    }
}

// メソッドレベルのセキュリティ
@Service
public class OrderService {
    
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOrderOwner(authentication, #orderId)")
    public Order getOrder(String orderId) {
        // 注文の取得ロジック
    }
    
    @PreAuthorize("hasRole('USER')")
    public Order createOrder(String customerId, List<OrderItemRequest> items) {
        // 注文の作成ロジック
    }
    
    @PreAuthorize("hasRole('ADMIN')")
    public List<Order> getAllOrders() {
        // 全注文の取得ロジック
    }
}

// カスタムセキュリティチェック
@Component
public class OrderSecurity {
    
    @Autowired
    private OrderRepository orderRepository;
    
    public boolean isOrderOwner(Authentication authentication, String orderId) {
        String username = authentication.getName();
        Order order = orderRepository.findById(orderId).orElse(null);
        
        if (order == null) {
            return false;
        }
        
        return order.getCustomerId().equals(username);
    }
}

6.2 トランザクション管理

6.2.1 基本概念

  • ACID特性: 原子性、一貫性、独立性、耐久性
  • トランザクション境界: トランザクションの開始と終了
  • トランザクション伝播: 既存のトランザクションとの関係
  • 分散トランザクション: 複数のリソースにまたがるトランザクション

6.2.2 実装例 (Spring Transaction)

@Service
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final CustomerRepository customerRepository;
    
    @Autowired
    public OrderService(
            OrderRepository orderRepository,
            ProductRepository productRepository,
            CustomerRepository customerRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.customerRepository = customerRepository;
    }
    
    @Transactional
    public Order createOrder(String customerId, List<OrderItemRequest> itemRequests) {
        // 顧客の存在確認
        Customer customer = customerRepository.findById(customerId)
                .orElseThrow(() -> new EntityNotFoundException("Customer not found"));
        
        // 注文の作成
        Order order = new Order();
        order.setId(UUID.randomUUID().toString());
        order.setCustomerId(customerId);
        order.setStatus(OrderStatus.DRAFT);
        order.setCreatedAt(LocalDateTime.now());
        
        // 注文明細の追加と在庫の更新
        for (OrderItemRequest itemRequest : itemRequests) {
            Product product = productRepository.findById(itemRequest.getProductId())
                    .orElseThrow(() -> new EntityNotFoundException("Product not found"));
            
            // 在庫チェック
            if (product.getStockQuantity() < itemRequest.getQuantity()) {
                throw new InsufficientStockException(
                        "Insufficient stock for product: " + product.getName());
            }
            
            // 在庫の減少
            product.decreaseStock(itemRequest.getQuantity());
            productRepository.save(product);
            
            // 注文明細の追加
            order.addItem(product, itemRequest.getQuantity());
        }
        
        // 注文の確定
        order.setStatus(OrderStatus.CONFIRMED);
        
        // 注文の保存
        return orderRepository.save(order);
    }
    
    @Transactional(readOnly = true)
    public Order getOrder(String orderId) {
        return orderRepository.findById(orderId)
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
    }
    
    @Transactional
    public void cancelOrder(String orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new EntityNotFoundException("Order not found"));
        
        if (order.getStatus() == OrderStatus.SHIPPED || order.getStatus() == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel a shipped or delivered order");
        }
        
        // 在庫の戻し
        for (OrderItem item : order.getItems()) {
            Product product = productRepository.findById(item.getProductId())
                    .orElseThrow(() -> new EntityNotFoundException("Product not found"));
            
            product.increaseStock(item.getQuantity());
            productRepository.save(product);
        }
        
        // 注文のキャンセル
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

6.3 例外処理

6.3.1 基本概念

  • 例外の種類: 検査例外と非検査例外
  • 例外の階層: 例外クラスの継承関係
  • 例外の変換: 低レベル例外から高レベル例外への変換
  • グローバル例外ハンドラー: アプリケーション全体での例外処理

6.3.2 実装例 (Spring Boot)

// カスタム例外
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
    
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class EntityNotFoundException extends BusinessException {
    public EntityNotFoundException(String message) {
        super(message);
    }
}

public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(String message) {
        super(message);
    }
}

// グローバル例外ハンドラー
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleEntityNotFoundException(EntityNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientStockException(InsufficientStockException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.toList());
        
        ErrorResponse error = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "Validation failed: " + String.join(", ", errors),
                LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        logger.error("Unhandled exception", ex);
        
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "An unexpected error occurred",
                LocalDateTime.now()
        );
        
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

// エラーレスポンスモデル
public class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;
    
    public ErrorResponse(int status, String message, LocalDateTime timestamp) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
    }
    
    // getters and setters
}

6.4 ロギングと監査

6.4.1 基本概念

  • ログレベル: DEBUG, INFO, WARN, ERROR, FATAL
  • 構造化ロギング: JSON形式などの構造化されたログ
  • 監査ログ: セキュリティ関連のイベントの記録
  • ログの集約: 分散システムでのログの集約

6.4.2 実装例 (Spring Boot + SLF4J + Logback)

// ロギングアスペクト
@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}
    
    @Around("serviceMethods()")
    public Object logServiceMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();
        
        logger.debug("Entering: {}.{}() with arguments: {}", className, methodName, Arrays.toString(args));
        
        try {
            Object result = joinPoint.proceed();
            logger.debug("Exiting: {}.{}() with result: {}", className, methodName, result);
            return result;
        } catch (Throwable e) {
            logger.error("Exception in {}.{}() with cause: {}", className, methodName, e.getMessage(), e);
            throw e;
        }
    }
}

// 監査ログアスペクト
@Aspect
@Component
public class AuditLogAspect {
    
    private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOG");
    
    @Autowired
    private HttpServletRequest request;
    
    @Pointcut("@annotation(com.example.annotation.Auditable)")
    public void auditableMethods() {}
    
    @Around("auditableMethods() && @annotation(auditable)")
    public Object logAuditEvent(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        // 認証情報の取得
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication != null ? authentication.getName() : "anonymous";
        
        // リクエスト情報の取得
        String ipAddress = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        
        // 監査ログの作成
        AuditLog log = new AuditLog();
        log.setTimestamp(LocalDateTime.now());
        log.setUsername(username);
        log.setIpAddress(ipAddress);
        log.setUserAgent(userAgent);
        log.setAction(auditable.action());
        log.setTarget(className + "." + methodName);
        log.setStatus("ATTEMPT");
        
        try {
            // メソッドの実行
            Object result = joinPoint.proceed();
            
            // 成功ログ
            log.setStatus("SUCCESS");
            auditLogger.info("{}", log);
            
            return result;
        } catch (Throwable e) {
            // 失敗ログ
            log.setStatus("FAILURE");
            log.setErrorMessage(e.getMessage());
            auditLogger.warn("{}", log);
            
            throw e;
        }
    }
}

// 監査ログアノテーション
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    String action();
}

// 監査ログモデル
public class AuditLog {
    private LocalDateTime timestamp;
    private String username;
    private String ipAddress;
    private String userAgent;
    private String action;
    private String target;
    private String status;
    private String errorMessage;
    
    // getters and setters
    
    @Override
    public String toString() {
        return new ObjectMapper().writeValueAsString(this);
    }
}

// サービスでの使用例
@Service
public class OrderService {
    
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    
    @Auditable(action = "CREATE_ORDER")
    @Transactional
    public Order createOrder(String customerId, List<OrderItemRequest> itemRequests) {
        logger.info("Creating order for customer: {}", customerId);
        
        // 注文作成ロジック
        
        logger.info("Order created successfully: {}", order.getId());
        return order;
    }
    
    @Auditable(action = "CANCEL_ORDER")
    @Transactional
    public void cancelOrder(String orderId) {
        logger.info("Cancelling order: {}", orderId);
        
        // 注文キャンセルロジック
        
        logger.info("Order cancelled successfully: {}", orderId);
    }
}

6.5 キャッシング

6.5.1 基本概念

  • キャッシュ戦略: 読み取り、書き込み、更新、削除
  • キャッシュの種類: ローカル、分散、多層
  • キャッシュの無効化: 期限切れ、明示的な無効化
  • キャッシュの一貫性: 強い一貫性、結果整合性

6.5.2 実装例 (Spring Cache)

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
                new ConcurrentMapCache("products"),
                new ConcurrentMapCache("customers"),
                new ConcurrentMapCache("orders")
        ));
        return cacheManager;
    }
}

@Service
public class ProductService {
    
    private final ProductRepository productRepository;
    
    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
    
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(String id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("Product not found"));
    }
    
    @Cacheable(value = "products", condition = "#price > 1000")
    public List<Product> getProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByPriceBetween(minPrice, maxPrice);
    }
    
    @CachePut(value = "products", key = "#result.id")
    public Product createProduct(ProductRequest request) {
        Product product = new Product();
        product.setId(UUID.randomUUID().toString());
        product.setName(request.getName());
        product.setDescription(request.getDescription());
        product.setPrice(request.getPrice());
        product.setStockQuantity(request.getStockQuantity());
        
        return productRepository.save(product);
    }
    
    @CachePut(value = "products", key = "#id")
    public Product updateProduct(String id, ProductRequest request) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("Product not found"));
        
        product.setName(request.getName());
        product.setDescription(request.getDescription());
        product.setPrice(request.getPrice());
        product.setStockQuantity(request.getStockQuantity());
        
        return productRepository.save(product);
    }
    
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(String id) {
        productRepository.deleteById(id);
    }
    
    @CacheEvict(value = "products", allEntries = true)
    public void clearProductCache() {
        // キャッシュのクリアのみ
    }
}

7. 設計パターンの選択と適用

7.1 要件に基づくパターン選択

7.1.1 ビジネス要件の分析

業務アプリケーションの設計パターンを選択する際は、まずビジネス要件を詳細に分析することが重要です:

  1. ビジネスドメインの複雑さ:

    • 複雑なドメインロジック → ドメインモデルパターン、DDD
    • 単純なCRUD操作が中心 → トランザクションスクリプト、アクティブレコード
  2. パフォーマンス要件:

    • 高いスループット → CQRS、キャッシング
    • 低レイテンシ → 最適化されたデータアクセス、非正規化
  3. スケーラビリティ要件:

    • 水平スケーリング → マイクロサービス、イベント駆動
    • 垂直スケーリング → モノリシック、最適化
  4. 保守性と拡張性:

    • 長期的な保守 → クリーンアーキテクチャ、ヘキサゴナルアーキテクチャ
    • 頻繁な変更 → モジュラー設計、疎結合

7.1.2 技術的制約の考慮

技術的な制約も設計パターンの選択に影響します:

  1. 既存のシステム環境:

    • レガシーシステムとの統合 → アダプターパターン、ファサード
    • 新規開発 → 最新のアーキテクチャパターン
  2. チームのスキルセット:

    • 経験豊富なチーム → 高度なパターン(DDD、CQRS、イベントソーシング)
    • 経験の浅いチーム → シンプルなパターン(レイヤードアーキテクチャ、MVC)
  3. 開発期間:

    • 短期間 → 既存のフレームワークやライブラリの活用
    • 長期間 → カスタマイズされたアーキテクチャ
  4. 予算:

    • 限られた予算 → コスト効率の高いソリューション
    • 十分な予算 → 最適なソリューション

7.2 パターンの組み合わせ

多くの場合、単一のパターンではなく、複数のパターンを組み合わせることで最適なソリューションが得られます:

7.2.1 相補的なパターンの組み合わせ

  • レイヤードアーキテクチャ + リポジトリパターン:

    • レイヤードアーキテクチャでシステム全体を構造化
    • リポジトリパターンでデータアクセスを抽象化
  • ドメインモデル + CQRS:

    • ドメインモデルで複雑なビジネスロジックを表現
    • CQRSで読み取りと更新を分離し、パフォーマンスを最適化
  • マイクロサービス + イベント駆動:

    • マイクロサービスでシステムを分割
    • イベント駆動でサービス間の疎結合を実現

7.2.2 パターンの階層的適用

  • アーキテクチャレベル: クリーンアーキテクチャ、マイクロサービス
  • アプリケーションレベル: MVC、MVVM、CQRS
  • ドメインレベル: ドメインモデル、トランザクションスクリプト
  • データアクセスレベル: リポジトリ、DAO、ORM
  • コンポーネントレベル: GoF設計パターン(ファクトリ、シングルトン、オブザーバーなど)

7.3 実践的な適用ガイドライン

7.3.1 段階的な導入

複雑なパターンを一度に導入するのではなく、段階的に導入することで、リスクを軽減し、学習曲線を緩やかにします:

  1. 基本的なレイヤードアーキテクチャから始める
  2. リポジトリパターンを導入してデータアクセスを抽象化
  3. ドメインモデルを徐々に豊かにする
  4. 必要に応じてCQRSを部分的に適用
  5. マイクロサービスへの移行は慎重に計画

7.3.2 プロトタイピングとフィードバック

新しいパターンを適用する前に、プロトタイプを作成し、フィードバックを得ることが重要です:

  1. 小規模なプロトタイプで概念を検証
  2. パフォーマンステストを実施
  3. チームからのフィードバックを収集
  4. 必要に応じて設計を調整

7.3.3 ドキュメンテーションと知識共有

選択したパターンとその適用方法を文書化し、チーム全体で知識を共有します:

  1. アーキテクチャ決定記録 (ADR) の作成
  2. 設計ドキュメントの維持
  3. コードレビューでの設計パターンの議論
  4. 定期的な知識共有セッションの開催

8. まとめと今後のトレンド

8.1 設計パターンの重要性

業務アプリケーションの設計パターンは、以下の理由から重要です:

  • 品質の向上: 検証済みのソリューションによる信頼性の確保
  • 開発効率の向上: 共通の語彙と構造による効率的な開発
  • 保守性の向上: 変更に強い設計による長期的な保守性
  • スケーラビリティの確保: 成長に対応できる柔軟な設計

8.2 今後のトレンド

業務アプリケーション設計の今後のトレンドには以下が含まれます:

8.2.1 クラウドネイティブアーキテクチャ

  • コンテナ化: Docker、Kubernetes
  • サーバーレス: AWS Lambda、Azure Functions
  • マイクロサービス: 細かく分割されたサービス
  • サービスメッシュ: Istio、Linkerd

8.2.2 イベント駆動アーキテクチャ

  • イベントソーシング: イベントを中心としたデータモデル
  • CQRS: 読み取りと更新の分離
  • リアクティブシステム: 非同期メッセージング
  • ストリーム処理: Kafka、Flink

8.2.3 AI/MLの統合

  • インテリジェントアプリケーション: AIを組み込んだアプリケーション
  • 予測分析: ビジネスデータからの予測
  • 自動化: 繰り返しタスクの自動化
  • パーソナライゼーション: ユーザー体験のカスタマイズ

8.2.4 ローコード/ノーコードプラットフォーム

  • ビジュアル開発: ドラッグアンドドロップインターフェース
  • 市民開発者: 技術的でないユーザーによる開発
  • ビジネスプロセス自動化: ワークフローの自動化
  • 統合プラットフォーム: 様々なシステムの統合

8.3 継続的な学習と適応

技術の急速な進化に対応するためには、継続的な学習と適応が不可欠です:

  • 新しい技術とパターンの探求
  • コミュニティへの参加
  • 実験と検証
  • フィードバックループの確立

業務アプリケーションの設計は、技術とビジネスの両方の理解が求められる複雑な分野です。適切な設計パターンの選択と適用により、効率的で保守性の高い、ビジネス価値を提供するシステムを構築することができます。

最終的には、特定のパターンに固執するのではなく、ビジネス要件と技術的制約のバランスを取りながら、最適なソリューションを柔軟に選択することが重要です。

関連記事

2025/3/25

【「動作保証はどこまで?」SIerのためのシステム保守の基本】

SIerエンジニアのためのシステム保守ガイド。業務システムの保守範囲の定義から具体的な保守活動まで、実践的なアプローチを解説します。

2025/3/24

【SIerが知るべきログ設計のベストプラクティス】

SIerエンジニアのためのログ設計ガイド。業務システムにおける効果的なログ設計から運用管理まで、実践的なベストプラクティスを解説します。

2025/3/23

【長年運用されている業務システムの"負債"とどう向き合うか?】

SIerエンジニアのための技術的負債管理ガイド。長年運用されてきた業務システムの負債を理解し、効果的に管理・改善していくための実践的なアプローチを解説します。