デザインパターンスキル
Java の実践的なデザインパターンリファレンス(最新のサンプル付き)。
使用タイミング
- ユーザーが特定のパターンの実装を依頼したとき
- 拡張可能/柔軟なコンポーネントを設計するとき
- 硬直化したコード構造をリファクタリングするとき
- コードレビューでパターンの使用が提案されたとき
クイックリファレンス:いつどのパターンを使うか
| 問題 | パターン |
|---|---|
| 複雑なオブジェクトの構築 | Builder |
| クラスを指定せずにオブジェクトを作成 | Factory |
| 複数のアルゴリズム、実行時に切り替え | Strategy |
| クラスを変更せずに振る舞いを追加 | Decorator |
| 複数のオブジェクトに変更を通知 | Observer |
| 単一インスタンスを保証 | Singleton |
| 互換性のないインターフェースを変換 | Adapter |
| アルゴリズムの骨格を定義 | Template Method |
生成に関するパターン
Builder
使用するケース: オブジェクトに多くのパラメーターがあり、一部はオプションの場合。
// ❌ テレスコーピングコンストラクタ(アンチパターン)
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... コンストラクタの爆発的な増加
}
// ✅ Builderパターン
public class User {
private final String name; // 必須
private final String email; // 必須
private final int age; // オプション
private final String phone; // オプション
private final String address; // オプション
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
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 String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// 使用例
User user = User.builder("John", "[email protected]")
.age(30)
.phone("+1234567890")
.build();
Lombokを使う場合:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
使用するケース: 具象クラスを指定せずにオブジェクトを作成する必要がある場合。
// ✅ Factory Methodパターン
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("メール: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("プッシュ: " + 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: " + type);
};
}
}
// 使用例
Notification notification = NotificationFactory.create("EMAIL");
notification.send("こんにちは!");
Springを使う場合(推奨):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@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 getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("不明なタイプ: " + type));
}
}
Singleton
使用するケース: インスタンスが正確に1つだけ必要な場合(使用は控えめに!)。
// ✅ モダンなSingleton(enumベース、スレッドセーフ)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// 接続を初期化
}
public Connection getConnection() {
return connection;
}
}
// 使用例
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Springを使う場合(推奨):
@Component // デフォルトスコープはシングルトン
public class DatabaseConnection {
// Springが単一インスタンスを管理
}
警告: Singletonには問題点があります:
- テストが難しい(グローバルステート)
- 依存関係が隠蔽される
- 代わりに依存性注入を検討しましょう
振る舞いに関するパターン
Strategy
使用するケース: 同じ操作に対して複数のアルゴリズムがあり、実行時に切り替える必要がある場合。
// ✅ Strategyパターン
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("カード " + cardNumber + " で " + amount + " を支払いました");
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("PayPal (" + email + ") で " + amount + " を支払いました");
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("ウォレット " + walletAddress + " に " + amount + " を支払いました");
}
}
// コンテキスト
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// 使用例
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// 実行時に戦略を変更
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Java 8+(関数型)の場合:
// 関数型インターフェースとしての戦略
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// ラムダ式を使った使用例
PaymentStrategy creditCard = amount ->
System.out.println("カード支払い: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("PayPal支払い: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
使用するケース: あるオブジェクトの変更を他のオブジェクトに通知する必要がある場合。
// ✅ Observerパターン(モダンJava)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// 注文処理
saveOrder(order);
// すべてのオブザーバーに通知
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// オブザーバー
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// 在庫を減らす
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// 設定
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Spring Eventsを使う場合(推奨):
// イベント
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) {
// 在庫を減らす
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// メールを送信
}
@EventListener
@Async // 非同期処理
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// 非同期でメールを送信
}
}
Template Method
使用するケース: アルゴリズムの骨格を定義し、サブクラスにステップの実装を任せる場合。
// ✅ Template Methodパターン
public abstract class DataProcessor {
// テンプレートメソッド - アルゴリズムを定義
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// サブクラスによって実装されるステップ
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// フック - 必要に応じてオーバーライド
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("処理が完了しました!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("CSVファイルを読み込み中...");
}
@Override
protected void processData() {
System.out.println("CSVデータを処理中...");
}
@Override
protected void writeData() {
System.out.println("データベースに書き込み中...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("APIから取得中...");
}
@Override
protected void processData() {
System.out.println("APIレスポンスを変換中...");
}
@Override
protected void writeData() {
System.out.println("キャッシュに書き込み中...");
}
@Override
protected boolean shouldNotify() {
return false; // フックをオーバーライド
}
}
// 使用例
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
構造に関するパターン
Decorator
使用するケース: 既存のクラスを変更せずに動的に振る舞いを追加する場合。
// ✅ Decoratorパターン
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "コーヒー";
}
@Override
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;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// 具象デコレーター
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + "、ミルク";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + "、砂糖";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + "、ホイップクリーム";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.70"));
}
}
// 使用例
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription() + " = $" + coffee.getCost());
// 出力: コーヒー、ミルク、砂糖、ホイップクリーム = $3.40
補足
アダプター
使用するケース: 互換性のないインターフェースを連携させる場合。
// サードパーティのクラス(変更不可)
public class LegacyLibrary {
public String fetchOldData() {
return "古いデータ";
}
}
// 期待されるインターフェース
public interface DataProvider {
String getData();
}
// ✅ Adapterパターン
public class LegacyAdapter implements DataProvider {
private final LegacyLibrary legacy;
public LegacyAdapter(LegacyLibrary legacy) {
this.legacy = legacy;
}
@Override
public String getData() {
return legacy.fetchOldData();
}
}
// 使用例
DataProvider provider = new LegacyAdapter(new LegacyLibrary());
String data = provider.getData();
重要な注意点:
- パターンは硬直したルールではなく、ガイドラインとして使用する
- 単純な解決策で十分な場合にパターンを過剰に適用しない
- パターンがもたらすトレードオフ(複雑さ、間接性)を常に考慮する
- Spring Framework は多くのパターン(DI、Proxy、Template Method)を内部的に実装しているため、手動実装よりもSpringの機能を優先する