design-patterns について
Java 開発者が課題に応じて一般的なデザインパターンを選べるクイックリファレンススキルです。実装例とともに、生成・振る舞い・構造のパターンやマイクロサービス向けパターンを扱います。
主な機能
- 01課題に応じた一般的なパターン選択
- 02Java によるパターン実装例
- 03生成・振る舞い・構造パターンを網羅
- 04Outbox、Saga、CQRS の解説
- 05選択指針とアンチパターンの紹介
活用シーン
- 01複雑なオブジェクト生成に Builder や Factory を選択
- 02交換可能なアルゴリズムに Strategy を適用
- 03データベース更新とイベント発行を扱う
- 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 | 動的な合成が必要な場合 |
| 複数のオブジェクトに変更を通知する | Observer | 1 対多の依存関係がある場合 |
| 互換性のないインターフェースを変換する | 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 | 重複処理が発生します | 冪等なコンシューマーと重複排除キー |
コメント
Markdown 対応