【プログラミングしてみよう61】Javaでデザインパターンをまとめる(Observer・Strategyなど)

Java

前回はApache CommonsとGuavaを使った外部ライブラリの活用を学びました。

今回は「デザインパターン」をまとめて解説します。 以前Singleton・Factoryを紹介しましたが、今回はObserver・Strategy・Decoratorという実践でよく登場する3つのパターンを追加で学びます。

デザインパターンの復習

以前学んだパターンを振り返ります。

Singleton  // インスタンスを1つに制限する
Factory    // インスタンス生成を1か所に集める
Builder    // フィールドが多いクラスの生成を読みやすくする

今回新たに学ぶパターンは以下の3つです。

Strategy   // アルゴリズムを切り替え可能にする
Observer   // 状態変化を他のオブジェクトに通知する
Decorator  // 機能を動的に追加する

Strategyパターン

「Strategyパターン」は、アルゴリズム(処理の方法)をインターフェースとして定義し、実行時に切り替えられるようにするパターンです。

たとえばソートのアルゴリズムを「バブルソート」「クイックソート」から選べるようにしたり、支払い方法を「クレジットカード」「電子マネー」から選べるようにしたりする場面に使います。

サンプルコード(Strategy)

ファイル作成

「Java49」フォルダに DiscountStrategy.java、NoDiscount.java、PercentDiscount.java、FixedDiscount.java、PriceCalculator.java、StrategyTest.java という名前でファイルを作成します

コード入力

DiscountStrategy.java に以下のコードを入力します。

@FunctionalInterface
public interface DiscountStrategy {
    int apply(int price);
}

NoDiscount.java に以下のコードを入力します。

public class NoDiscount implements DiscountStrategy {
    @Override
    public int apply(int price) {
        return price;
    }
}

PercentDiscount.java に以下のコードを入力します。

public class PercentDiscount implements DiscountStrategy {
    private final int percent;

    public PercentDiscount(int percent) {
        this.percent = percent;
    }

    @Override
    public int apply(int price) {
        return price * (100 - percent) / 100;
    }
}

FixedDiscount.java に以下のコードを入力します。

public class FixedDiscount implements DiscountStrategy {
    private final int amount;

    public FixedDiscount(int amount) {
        this.amount = amount;
    }

    @Override
    public int apply(int price) {
        return Math.max(0, price - amount);
    }
}

PriceCalculator.java に以下のコードを入力します。

public class PriceCalculator {
    private DiscountStrategy strategy;

    public PriceCalculator(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    // 実行時にStrategyを切り替えられる
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public int calculate(int price) {
        return strategy.apply(price);
    }
}

StrategyTest.java に以下のコードを入力します。

public class StrategyTest {
    public static void main(String[] args) {
        int originalPrice = 1000;
        PriceCalculator calc = new PriceCalculator(new NoDiscount());

        System.out.println("元の値段: " + originalPrice + "円");
        System.out.println("割引なし: " + calc.calculate(originalPrice) + "円");

        calc.setStrategy(new PercentDiscount(20));
        System.out.println("20%割引: " + calc.calculate(originalPrice) + "円");

        calc.setStrategy(new FixedDiscount(300));
        System.out.println("300円引き: " + calc.calculate(originalPrice) + "円");

        // ラムダ式でその場でStrategyを定義
        calc.setStrategy(price -> price / 2);
        System.out.println("半額: " + calc.calculate(originalPrice) + "円");
    }
}

コンパイル

javac DiscountStrategy.java NoDiscount.java PercentDiscount.java FixedDiscount.java PriceCalculator.java StrategyTest.java

実行

java StrategyTest

実行結果は次のようになります。

元の値段: 1000円
割引なし: 1000円
20%割引: 800円
300円引き: 700円
半額: 500円

Observerパターン

「Observerパターン」は、オブジェクトの状態変化を別のオブジェクトに自動的に通知する仕組みです。「出版-購読(Pub-Sub)」パターンとも呼ばれ、GUIのイベント処理やチャットシステムなどに広く使われています。

サンプルコード(Observer)

ファイル作成

「Java49」フォルダに EventListener.java、EventSource.java、ObserverTest.java という名前でファイルを作成します

コード入力

EventListener.java に以下のコードを入力します。

@FunctionalInterface
public interface EventListener<T> {
    void onEvent(T event);
}

EventSource.java に以下のコードを入力します。

import java.util.ArrayList;
import java.util.List;

public class EventSource<T> {
    private final List<EventListener<T>> listeners = new ArrayList<>();

    public void addListener(EventListener<T> listener) {
        listeners.add(listener);
    }

    public void removeListener(EventListener<T> listener) {
        listeners.remove(listener);
    }

    public void emit(T event) {
        for (EventListener<T> listener : listeners) {
            listener.onEvent(event);
        }
    }
}

ObserverTest.java に以下のコードを入力します。

public class ObserverTest {
    record StockEvent(String product, int quantity) {}

    public static void main(String[] args) {
        EventSource<StockEvent> stockAlert = new EventSource<>();

        // ラムダ式でリスナーを登録
        stockAlert.addListener(event ->
            System.out.println("[メール通知] " + event.product() + "の在庫が" + event.quantity() + "個になりました"));

        stockAlert.addListener(event ->
            System.out.println("[Slack通知] 在庫警告: " + event.product() + " 残り" + event.quantity() + "個"));

        stockAlert.addListener(event -> {
            if (event.quantity() == 0) {
                System.out.println("[緊急通知] " + event.product() + "が在庫切れです!");
            }
        });

        System.out.println("--- 在庫5個 ---");
        stockAlert.emit(new StockEvent("コーヒー", 5));

        System.out.println("\n--- 在庫0個 ---");
        stockAlert.emit(new StockEvent("コーヒー", 0));
    }
}

コンパイル

javac EventListener.java EventSource.java ObserverTest.java

実行

java ObserverTest

実行結果は次のようになります。

--- 在庫5個 ---
[メール通知] コーヒーの在庫が5個になりました
[Slack通知] 在庫警告: コーヒー 残り5個

--- 在庫0個 ---
[メール通知] コーヒーの在庫が0個になりました
[Slack通知] 在庫警告: コーヒー 残り0個
[緊急通知] コーヒーが在庫切れです!

Decoratorパターン

「Decoratorパターン」は、既存のクラスを変更せずに機能を動的に追加するパターンです。コーヒーのトッピングのように、基本の機能に追加の機能を重ねていくイメージです。

サンプルコード(Decorator)

ファイル作成

「Java49」フォルダに TextProcessor.java、BaseProcessor.java、TrimDecorator.java、UpperCaseDecorator.java、PrefixDecorator.java、DecoratorTest.java という名前でファイルを作成します

コード入力

TextProcessor.java に以下のコードを入力します。

@FunctionalInterface
public interface TextProcessor {
    String process(String text);

    default TextProcessor andThen(TextProcessor after) {
        return text -> after.process(this.process(text));
    }
}

BaseProcessor.java に以下のコードを入力します。

public class BaseProcessor implements TextProcessor {
    @Override
    public String process(String text) {
        return text;
    }
}

TrimDecorator.java に以下のコードを入力します。

public class TrimDecorator implements TextProcessor {
    private final TextProcessor wrapped;

    public TrimDecorator(TextProcessor wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public String process(String text) {
        return wrapped.process(text).trim();
    }
}

UpperCaseDecorator.java に以下のコードを入力します。

public class UpperCaseDecorator implements TextProcessor {
    private final TextProcessor wrapped;

    public UpperCaseDecorator(TextProcessor wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public String process(String text) {
        return wrapped.process(text).toUpperCase();
    }
}

PrefixDecorator.java に以下のコードを入力します。

public class PrefixDecorator implements TextProcessor {
    private final TextProcessor wrapped;
    private final String prefix;

    public PrefixDecorator(TextProcessor wrapped, String prefix) {
        this.wrapped = wrapped;
        this.prefix  = prefix;
    }

    @Override
    public String process(String text) {
        return prefix + wrapped.process(text);
    }
}

DecoratorTest.java に以下のコードを入力します。

public class DecoratorTest {
    public static void main(String[] args) {
        String input = "  hello world  ";

        // クラスを使ったDecorator
        TextProcessor processor = new PrefixDecorator(
                new UpperCaseDecorator(
                        new TrimDecorator(
                                new BaseProcessor())), ">>> ");

        System.out.println("入力: \"" + input + "\"");
        System.out.println("クラスDecorator: " + processor.process(input));

        // ラムダ式とandThenで同じことを書く
        TextProcessor pipeline = ((TextProcessor) String::trim)
                .andThen(String::toUpperCase)
                .andThen(s -> ">>> " + s);

        System.out.println("ラムダDecorator: " + pipeline.process(input));

        // 異なる組み合わせを動的に作る
        System.out.println("\n--- 組み合わせのバリエーション ---");
        TextProcessor trimOnly   = new TrimDecorator(new BaseProcessor());
        TextProcessor upperOnly  = new UpperCaseDecorator(new BaseProcessor());
        TextProcessor trimUpper  = new UpperCaseDecorator(new TrimDecorator(new BaseProcessor()));

        System.out.println("trimのみ:  \"" + trimOnly.process(input) + "\"");
        System.out.println("大文字のみ: \"" + upperOnly.process(input) + "\"");
        System.out.println("trim+大文字: \"" + trimUpper.process(input) + "\"");
    }
}

コンパイル

javac TextProcessor.java BaseProcessor.java TrimDecorator.java UpperCaseDecorator.java PrefixDecorator.java DecoratorTest.java

実行

java DecoratorTest

実行結果は次のようになります。

入力: "  hello world  "
クラスDecorator: >>> HELLO WORLD
ラムダDecorator: >>> HELLO WORLD

--- 組み合わせのバリエーション ---
trimのみ:  "hello world"
大文字のみ: "  HELLO WORLD  "
trim+大文字: "HELLO WORLD"

コードの解説

Strategyパターンでは DiscountStrategy を @FunctionalInterface にすることで、ラムダ式でその場で戦略を定義できます。PriceCalculator はStrategyのインターフェースに依存しているため、具体的な割引計算を知らなくてもよく、実行時に setStrategy() で切り替えられます。

Observerパターンでは EventSource がリスナーのリストを管理し、emit() を呼ぶことですべてのリスナーに通知します。リスナーも @FunctionalInterface にすることでラムダ式で登録できます。通知する側(EventSource)と通知を受ける側(リスナー)が分離されているため、新しい通知先を追加してもEventSourceを変更する必要がありません。

Decoratorパターンでは TextProcessor インターフェースを TrimDecorator・UpperCaseDecorator・PrefixDecorator がそれぞれ実装しつつ、コンストラクタで別の TextProcessor を受け取って包み込みます(Wrapping)。このネスト構造により、実行時に組み合わせを自由に変えられます。TextProcessor を @FunctionalInterface にすることでラムダ式と andThen でも同じことが書けます。

まとめ

Strategyパターンはアルゴリズムをインターフェースとして切り出すことで実行時に切り替えられるようにし、Observerパターンは状態変化をリスナーに通知することで通知する側と受ける側を分離し、Decoratorパターンは機能を重ねていくことで既存クラスを変更せずに機能を追加します。どのパターンも @FunctionalInterface とラムダ式を組み合わせることで、クラスファイルを増やさずにシンプルに書けます。これまで学んだ継承・インターフェース・ラムダ式・関数型インターフェースの知識がデザインパターンの実装に活きています。

2026/08/09(日) ぱんまる

コメント

タイトルとURLをコピーしました