mcpField
D
design-patterns

design-patterns

github.com/piomin開発1.4k63 回インストール5 日前に更新
リポジトリ

design-patterns について

Java 開発者が課題に応じて一般的なデザインパターンを選べるクイックリファレンススキルです。実装例とともに、生成・振る舞い・構造のパターンやマイクロサービス向けパターンを扱います。

主な機能

  1. 01課題に応じた一般的なパターン選択
  2. 02Java によるパターン実装例
  3. 03生成・振る舞い・構造パターンを網羅
  4. 04Outbox、Saga、CQRS の解説
  5. 05選択指針とアンチパターンの紹介

活用シーン

  1. 01複雑なオブジェクト生成に Builder や Factory を選択
  2. 02交換可能なアルゴリズムに Strategy を適用
  3. 03データベース更新とイベント発行を扱う
  4. 04マイクロサービスで Saga や CQRS を検討

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/piomin/claude-ai-spring-boot --skill design-patterns

インストール後、Claude Code で/design-patternsを入力して読み込まれたか確認します。

詳細

デザインパターン スキル

Java の一般的なデザインパターンのクイックリファレンスです。

使用するタイミング

  • ユーザーが特定のパターンの実装を依頼した場合
  • 拡張性や柔軟性のあるコンポーネントを設計する場合
  • 硬直化したコードをリファクタリングする場合

クイックリファレンス:パターンの選び方

問題パターン使用するタイミング
複雑なオブジェクトの構築Builderパラメーターが多く、一部がオプションの場合
クラスを指定せずにオブジェクトを作成するFactory実行時に型が決まる場合
複数のアルゴリズムを実行時に切り替えるStrategyコンテキストによって動作が変わる場合
クラスを変更せずに動作を追加するDecorator動的な合成が必要な場合
複数のオブジェクトに変更を通知するObserver1 対多の依存関係がある場合
互換性のないインターフェースを変換するAdapterレガシーコードやサードパーティーコードを統合する場合
DB 更新とイベント発行をアトミックに行うOutboxマイクロサービスにおける二重書き込み問題
サービス間の分散トランザクションSaga複数サービスにまたがるビジネスプロセス
読み取りモデルと書き込みモデルを分離するCQRS読み取りと書き込みの比率に大きな偏りがある場合

生成パターン

Builder

問題: 段階的に増えたコンストラクター、多数のオプションパラメーター

// ✅ Builder pattern
public class User {
    private final String name;  // required
    private final String email; // required
    private final int age;      // optional

    private User(Builder builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
    }

    public static Builder builder(String name, String email) {
        return new Builder(name, email);
    }

    public static class Builder {
        private final String name;
        private final String email;
        private int age = 0;

        private Builder(String name, String email) {
            this.name = name;
            this.email = email;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// Usage
User user = User.builder("John", "[email protected]")
    .age(30)
    .build();
Factory

問題: 正確なクラスが事前に分からない状態でオブジェクトを作成する

// ✅ Factory pattern
public interface Notification {
    void send(String message);
}

public class NotificationFactory {
    public static Notification create(String type) {
        return switch (type.toUpperCase()) {
            case "EMAIL" -> new EmailNotification();
            case "SMS" -> new SmsNotification();
            case "PUSH" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown: " + type);
        };
    }
}

// Spring version - preferred
@Component
public class NotificationFactory {
    private final Map<String, NotificationSender> senders;

    public NotificationFactory(List<NotificationSender> senderList) {
        this.senders = senderList.stream()
            .collect(Collectors.toMap(
                NotificationSender::getType,
                Function.identity()
            ));
    }

    public NotificationSender get(String type) {
        return Optional.ofNullable(senders.get(type))
            .orElseThrow(() -> new IllegalArgumentException("Unknown: " + type));
    }
}

振る舞いパターン

Strategy

問題: 同じ操作に対して複数のアルゴリズムがあり、実行時に選択する

// ✅ Strategy pattern
public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

public class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " with card " + cardNumber);
    }
}

public class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(BigDecimal total) {
        paymentStrategy.pay(total);
    }
}

// Usage
cart.setPaymentStrategy(new CreditCardPayment("4111..."));
cart.checkout(new BigDecimal("99.99"));

// Functional variant (Java 8+)
@FunctionalInterface
public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

PaymentStrategy creditCard = amount -> System.out.println("Card: " + amount);
cart.setPaymentStrategy(creditCard);
Observer

問題: 状態が変化したときに複数のオブジェクトに通知する

// ✅ Spring Events (preferred)
public record OrderPlacedEvent(Order order) {}

@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public void placeOrder(Order order) {
        saveOrder(order);
        eventPublisher.publishEvent(new OrderPlacedEvent(order));
    }
}

@Component
public class InventoryListener {
    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // Reduce inventory
    }
}

@Component
public class EmailListener {
    @EventListener
    @Async
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // Send email
    }
}

構造パターン

Decorator

問題: クラスを変更せずに動作を動的に追加する

// ✅ Decorator pattern
public interface Coffee {
    String getDescription();
    BigDecimal getCost();
}

public class SimpleCoffee implements Coffee {
    public String getDescription() { return "Coffee"; }
    public BigDecimal getCost() { return new BigDecimal("2.00"); }
}

public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee coffee;
    public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}

public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }

    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    public BigDecimal getCost() {
        return coffee.getCost().add(new BigDecimal("0.50"));
    }
}

// Usage
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
Adapter

問題: 互換性のないインターフェースを連携させる

// ✅ Adapter pattern
public interface MediaPlayer {
    void play(String filename);
}

// Legacy code
public class LegacyAudioPlayer {
    public void playMp3(String filename) { /* ... */ }
}

// Adapter
public class Mp3PlayerAdapter implements MediaPlayer {
    private final LegacyAudioPlayer legacyPlayer = new LegacyAudioPlayer();

    @Override
    public void play(String filename) {
        legacyPlayer.playMp3(filename);
    }
}

// Usage
MediaPlayer player = new Mp3PlayerAdapter();
player.play("song.mp3");

エンタープライズ/マイクロサービスパターン

Transactional Outbox

問題: データベースを更新し、イベントを発行する操作をアトミックに行う(二重書き込み問題)

// ✅ Outbox pattern - write event to DB in same transaction, poll and publish later
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String aggregateType;
    private String aggregateId;
    private String eventType;

    @Column(columnDefinition = "TEXT")
    private String payload;

    private LocalDateTime createdAt;
    private boolean published;

    // getters/setters
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final OutboxEventRepository outboxRepository;

    public OrderService(OrderRepository orderRepository, OutboxEventRepository outboxRepository) {
        this.orderRepository = orderRepository;
        this.outboxRepository = outboxRepository;
    }

    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setStatus(OrderStatus.CREATED);
        order = orderRepository.save(order);

        OutboxEvent event = new OutboxEvent();
        event.setAggregateType("Order");
        event.setAggregateId(order.getId().toString());
        event.setEventType("OrderCreated");
        event.setPayload(toJson(order));
        event.setCreatedAt(LocalDateTime.now());
        event.setPublished(false);
        outboxRepository.save(event);

        return order;
    }
}

// Poller publishes events and marks them as published
@Component
public class OutboxPoller {
    private final OutboxEventRepository outboxRepository;
    private final KafkaTemplate<String, String> kafka;

    public OutboxPoller(OutboxEventRepository outboxRepository,
                        KafkaTemplate<String, String> kafka) {
        this.outboxRepository = outboxRepository;
        this.kafka = kafka;
    }

    @Scheduled(fixedDelay = 1000)
    @Transactional
    public void publishPendingEvents() {
        List<OutboxEvent> events = outboxRepository.findByPublishedFalse();
        for (OutboxEvent event : events) {
            kafka.send(event.getAggregateType(), event.getAggregateId(), event.getPayload());
            event.setPublished(true);
        }
    }
}
Saga(コレオグラフィー)

問題: 複数のマイクロサービスにまたがる分散トランザクション

// ✅ Choreography-based saga - each service listens and reacts
// Order Service publishes OrderCreated
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    private final OrderRepository orderRepository;

    public OrderService(ApplicationEventPublisher eventPublisher, OrderRepository orderRepository) {
        this.eventPublisher = eventPublisher;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setStatus(OrderStatus.PENDING);
        order = orderRepository.save(order);
        eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), request.items()));
        return order;
    }

    @Transactional
    public void handlePaymentFailed(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

// Payment Service listens to OrderCreated, publishes PaymentCompleted or PaymentFailed
@Component
public class PaymentEventHandler {
    private final PaymentService paymentService;

    public PaymentEventHandler(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @KafkaListener(topics = "order-events")
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            paymentService.charge(event.orderId(), event.amount());
            // Publish PaymentCompleted
        } catch (PaymentException e) {
            // Publish PaymentFailed → triggers compensating action
        }
    }
}

コレオグラフィーとオーケストレーションの比較:

項目コレオグラフィーオーケストレーション
結合度低い — サービスはイベントに反応します高い — オーケストレーターがすべてのステップを把握します
複雑さフロー全体の追跡が困難です集中管理され、理解しやすくなります
使用するタイミングステップが少なく、フローが単純な場合ステップが多く、補償処理が複雑な場合
CQRS(コマンドクエリ責務分離)

問題: 読み取りモデルと書き込みモデルで、最適化の要件が異なる

// ✅ CQRS - separate command and query models

// Command side - normalized, validates business rules
@Service
@Transactional
public class OrderCommandService {
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    public OrderCommandService(OrderRepository orderRepository,
                                ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    public Long createOrder(CreateOrderCommand command) {
        Order order = new Order();
        order.setCustomerId(command.customerId());
        order.setStatus(OrderStatus.CREATED);
        command.items().forEach(order::addItem);
        order = orderRepository.save(order);
        eventPublisher.publishEvent(new OrderCreatedEvent(order));
        return order.getId();
    }
}

// Query side - denormalized, optimized for reads
@Service
@Transactional(readOnly = true)
public class OrderQueryService {
    private final OrderViewRepository viewRepository;

    public OrderQueryService(OrderViewRepository viewRepository) {
        this.viewRepository = viewRepository;
    }

    public OrderView findById(Long id) {
        return viewRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Order not found"));
    }

    public Page<OrderView> findByCustomer(Long customerId, Pageable pageable) {
        return viewRepository.findByCustomerId(customerId, pageable);
    }
}

// Read model - flat, pre-joined
@Entity
@Table(name = "order_views")
public class OrderView {
    @Id
    private Long id;
    private Long customerId;
    private String customerName;
    private String status;
    private BigDecimal totalAmount;
    private int itemCount;
    private LocalDateTime createdAt;

    // getters/setters
}

// Projection updater - keeps query model in sync
@Component
public class OrderViewUpdater {

    private final OrderViewRepository viewRepository;

    public OrderViewUpdater(OrderViewRepository viewRepository) {
        this.viewRepository = viewRepository;
    }

    @EventListener
    public void on(OrderCreatedEvent event) {
        OrderView view = new OrderView();
        view.setId(event.order().getId());
        view.setCustomerId(event.order().getCustomerId());
        view.setStatus(event.order().getStatus().name());
        view.setTotalAmount(event.order().getTotalAmount());
        view.setItemCount(event.order().getItems().size());
        view.setCreatedAt(event.order().getCreatedAt());
        viewRepository.save(view);
    }
}

CQRS を使用するタイミング:

  • 読み取りと書き込みの比率に大きな偏りがある(読み取りが多く、書き込みが少ない)
  • 読み取りモデルに、書き込みモデルとは異なる形状が必要(非正規化ビュー)
  • 読み取りと書き込みで異なるスケーリング要件がある

使用しないタイミング: 単純な CRUD アプリケーションでは、不要な複雑さが加わります。


パターン選択ガイド

状況パターン
オブジェクトの生成が複雑Builder、Factory
機能を動的に追加する必要があるDecorator
アルゴリズムに複数の実装があるStrategy
状態の変化に反応するObserver(Spring Events)
レガシーコードと統合するAdapter
DB を更新し、イベントをアトミックに発行するOutbox
サービス間の分散トランザクションSaga
読み取りと書き込みの最適化を分離するCQRS

避けるべきアンチパターン

アンチパターン問題より良い方法
Singleton の乱用グローバル状態になり、テストが困難になります依存性注入
あらゆる場面で Factory を使う過剰設計になります型が分かっている場合は単純に new を使います
深い Decorator チェーンデバッグが困難になります合成を使い、チェーンを短く保ちます
単純な CRUD に CQRS を使う不要な複雑さが加わります単一モデルと標準的なリポジトリー
冪等性のない Saga重複処理が発生します冪等なコンシューマーと重複排除キー

コメント