Open24

Java言語で学ぶデザインパターン入門 まとめ

raamenwakamaturaamenwakamatu

はじめに

※この内容は「Java言語で学ぶデザインパターン入門」を読んで、自分の理解をまとめたものです。
※ソースコードやコメントは書籍の趣旨と異なる解釈をしている可能性があります。あくまで私の理解・整理用メモです。

raamenwakamaturaamenwakamatu

Iteratorパターン

概要

集合(コレクション)に対して順番に要素を指し示し、全体をスキャンしながら処理を繰り返すパターン


実装コード

Taskクラス

public class Task {
    private String title;

    public Task(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

TaskListクラス(集合)

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

public class TaskList implements Iterable<Task> {
    private List<Task> tasks;

    public TaskList() {
        this.tasks = new ArrayList<>();
    }

    public Task getTaskAt(int index) {
        return tasks.get(index);
    }

    public void addTask(Task task) {
        tasks.add(task);
    }

    public int getSize() {
        return tasks.size();
    }

    @Override
    public Iterator<Task> iterator() {
        return new TaskListIterator(this);
    }
}

 

TaskListIteratorクラス(イテレータ)

import java.util.Iterator;
import java.util.NoSuchElementException;

public class TaskListIterator implements Iterator<Task> {
    private TaskList taskList;
    private int index;

    public TaskListIterator(TaskList taskList) {
        this.taskList = taskList;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        return index < taskList.getSize();
    }

    @Override
    public Task next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Task task = taskList.getTaskAt(index);
        index++;
        return task;
    }
}

Mainクラス(実行)

public class Main {
    public static void main(String[] args) {
        TaskList taskList = new TaskList();
        taskList.addTask(new Task("Buy groceries"));
        taskList.addTask(new Task("Clean the house"));
        taskList.addTask(new Task("Read a book"));
        taskList.addTask(new Task("Go for a run"));

        Iterator<Task> it = taskList.iterator();
        while (it.hasNext()) {
            Task task = it.next();
            System.out.println(task.getTitle());
        }
    }
}


✅ Iteratorパターンを使うメリット

  • 実装と繰り返し処理を切り離せる
    • Main クラスは TaskListIterator にしか依存していない。
  • TaskList の内部構造が変わっても影響が少ない
    • たとえば ArrayList から Task[] に変更しても、while のループ処理を変更せずに済む。

所感

  • 単にループ処理をするだけなのに、ここまでクラスを分けるのはやや大げさに感じた。
  • Book は値オブジェクトとして、BookShelf はファーストクラスコレクションとして設計すれば、副作用なくすっきり実装できる
  • next() メソッドは次の要素を返すだけでなく、インスタンス変数を直接書き換えるという副作用を持つ処理になっているのが少しわかりにくい。
raamenwakamaturaamenwakamatu

Adapterパターン

概要

Adapterパターンは、すでに提供されているもの(Adaptee)と、使いたいインターフェース(Target)の間にあるずれを埋めるためのデザインパターン

現実の開発では、古いライブラリや外部APIなどが、今の設計にそのままフィットしないことが多い。そのズレをAdapterで解決するのがこのパターンの目的。


実装コード

Adaptee(既存のクラス)

整数型のIDから顧客情報を取得する、古いAPI。

public class LegacyCustomerFetcher {
    public Customer getCustomerById(int id) {
        // 仮の実装
        return new Customer(String.valueOf(id));
    }
}

Target(目的のインターフェース)

アプリケーション側が使いたい新しいインターフェース。文字列型のIDで顧客情報を取得したい。

public interface CustomerRepository {
    Customer findById(String id);
}

Adapter(橋渡しをするクラス)

Adaptee を内部に持ち、新しい Target インターフェースを実装する。

public class LegacyCustomerAdapter implements CustomerRepository {
    private LegacyCustomerFetcher legacyFetcher;

    public LegacyCustomerAdapter(LegacyCustomerFetcher legacyFetcher) {
        this.legacyFetcher = legacyFetcher;
    }

    @Override
    public Customer findById(String id) {
        int numericId;
        try {
            numericId = Integer.parseInt(id);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("ID must be numeric for legacy fetcher");
        }
        return legacyFetcher.getCustomerById(numericId);
    }
}

Adapterパターンを使うメリット

  • オープン・クローズドの原則に従える(既存コードを変更せず、拡張のみで対応できる)。
  • レガシーなクラスに手を加えることなく再利用できる
  • Adapterを介することで、依存関係を最小限に抑えて移行可能。

所感

これまで、現場で新機能を開発する際に既存のクラスに分岐処理を追加して対応するケースを経験してきたが、それだと既存機能を壊してしまう可能性が高い。

Adapterパターンを使えば、既存コードは一切変更せずに、新しいインターフェース(Target)とその実装(Adapter)を追加するだけで、新仕様に対応できる。

raamenwakamaturaamenwakamatu

Template Methodパターン

概要

Template Method(テンプレートメソッド)パターンは、処理の枠組み(アルゴリズムの流れ)をスーパークラスで定義し、具体的な内容をサブクラスに任せるデザインパターン


実装コード

スーパークラス:テンプレートの定義

public abstract class DataProcessor {
    // テンプレートメソッド(サブクラスでオーバーライド不可にする)
    public final void process() {
        readData();
        processData();
        saveResults();
    }

    protected abstract void readData();      // サブクラスが定義する部分
    protected abstract void processData();   // サブクラスが定義する部分

    protected void saveResults() {
        System.out.println("結果を保存しました。");
    }
}

サブクラス1:ファイルから処理

public class FileDataProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("ファイルからデータを読み込みました。");
    }

    @Override
    protected void processData() {
        System.out.println("ファイルデータを処理しました。");
    }
}

サブクラス2:データベースから処理

public class DatabaseDataProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("データベースからデータを読み込みました。");
    }

    @Override
    protected void processData() {
        System.out.println("データベースデータを処理しました。");
    }
}

Mainクラス:使ってみる

public class Main {
    public static void main(String[] args) {
        DataProcessor fileProcessor = new FileDataProcessor();
        fileProcessor.process();

        System.out.println("----");

        DataProcessor dbProcessor = new DatabaseDataProcessor();
        dbProcessor.process();
    }
}

Template Method パターンのメリット

  • 共通処理をスーパークラスで定義できるため、重複を防げる
  • サブクラスは個別処理に集中できる(処理の順序は親が制御)
  • オープン/クローズドの原則に従った拡張が可能(変更には継承で対応)

所感

例えばバックエンドAPIの設計では、

  • リクエストパラメータの検証
  • リポジトリからのデータ取得
  • レスポンスデータの生成

といった共通の処理フローがありつつも、各ステップの中身だけが異なるという場面はよくある。

このようなケースでは、Template Methodパターンが適しているように思えるが、実際には処理の“流れ”自体が頻繁に変わるわけではないため、

「親クラスだけを変更すれば、他の具象クラスは手を加えずに済む」

という恩恵を受ける場面はそれほど多くないと感じている。

むしろ、Template Methodパターンの本質的なメリットは、

  • 共通の流れを親クラスに集約することで、具象クラスの記述量を減らせること
  • 新しいパターンを追加する際に、必要最低限のコードだけで拡張できること

にあると思う。

raamenwakamaturaamenwakamatu

Factory Method パターン

概要

Factory Method パターンは、インスタンスの生成処理(生成+DBへの登録など)という「流れ」だけをスーパークラス側で定めつつ、具体的な生成処理や登録処理はサブクラスに委ねるというデザインパターン。

言わば、テンプレートメソッドパターンを「インスタンス生成」に応用した形


実装

// Product:生成される製品の抽象クラス
public abstract class Product {
    public abstract void use();
}
// Factory:生成と登録の流れを定めた抽象クラス
public abstract class Factory {
    public final Product create(String owner) {
        Product p = createProduct(owner);  // 生成処理(サブクラスに委譲)
        registerProduct(p);                // 登録処理(サブクラスに委譲)
        return p;
    }

    protected abstract Product createProduct(String owner);
    protected abstract void registerProduct(Product product);
}

// AirCon:具体的な製品クラス
public class AirCon extends Product {
    private String owner;

    // パッケージプライベートにして外部からnewできないようにする
    AirCon(String owner) {
        this.owner = owner;
        System.out.println(owner + " のエアコンを生成しました。");
    }

    @Override
    public void use() {
        System.out.println(owner + " のエアコンを稼働させます。");
    }

    public String getOwner() {
        return owner;
    }
}
// AirConFactory:具体的なファクトリクラス
import java.util.ArrayList;
import java.util.List;

public class AirConFactory extends Factory {
    private List<String> owners = new ArrayList<>();

    @Override
    protected Product createProduct(String owner) {
        return new AirCon(owner);
    }

    @Override
    protected void registerProduct(Product product) {
        if (product instanceof AirCon airCon) {
            // 仮:DB登録など
            owners.add(airCon.getOwner());
        }
    }

    public List<String> getOwners() {
        return owners;
    }
}

実行例:Mainクラス

public class Main {
    public static void main(String[] args) {
        Factory factory = new AirConFactory();

        Product ac1 = factory.create("田中");
        Product ac2 = factory.create("佐藤");

        ac1.use();
        ac2.use();
    }
}

メリット

  • 共通処理の再利用がしやすい:インスタンス生成 → 登録という「共通の流れ」をテンプレート化できる。
  • 新しい製品を柔軟に追加できる:具象クラスの追加だけで対応可能。
  • OCP(オープン・クローズドの原則)に沿う:既存のコードを変更せずに拡張できる。

所感

インスタンスを生成して、そのままDB登録やリスト追加といった「一連の流れ」が毎回必要なケースでは、このパターンを使うことで ロジックの重複を減らし、可読性も向上することができると思う。

raamenwakamaturaamenwakamatu

Singletonパターン

概要

インスタンスが一個しか存在しないことを保証するパターン


実装例:共通のDBコネクションを使い回す

DatabaseManager.java

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseManager {
    private static final String URL = "jdbc:mysql://localhost:3306/sampledb";
    private static final String USER = "user";
    private static final String PASSWORD = "password";

    // 唯一のインスタンス
    private static DatabaseManager instance = new DatabaseManager();

    // DBコネクション
    private Connection connection;

    // private コンストラクタで外部からのインスタンス化を防ぐ
    private DatabaseManager() {
        try {
            connection = DriverManager.getConnection(URL, USER, PASSWORD);
            System.out.println("DB接続成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // インスタンス取得用メソッド
    public static DatabaseManager getInstance() {
        return instance;
    }

    // コネクション取得メソッド
    public Connection getConnection() {
        return connection;
    }
}

Main.java

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) {
        try {
            DatabaseManager dbManager = DatabaseManager.getInstance();
            Connection conn = dbManager.getConnection();

            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");

            while (rs.next()) {
                System.out.println(rs.getString("name"));
            }

            rs.close();
            stmt.close();
            // Connection はアプリ終了時まで使い回す

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

メリット

  • 同じ接続を何度も作らずに済む
  • 1つのコネクションに固定するのは現実的ではないが、
    複数のコネクションを1つの接続管理クラス(シングルトン)で管理する」のがベター

所感

  • ロギングのインスタンスや設定管理(アプリ全体の設定ファイルの読み込み)など、
    • グローバル状態を管理するクラス
    • 複数インスタンスを作成する必要がないクラス
  • DB接続インスタンスのように、無駄な接続数を増やしたくないケースには適用すると良さそう。
raamenwakamaturaamenwakamatu

Prototypeパターン

概要

既存のインスタンスをコピーして新しいインスタンスを作成するデザインパターン。

実装コード

Prototype

public interface Shape extends Cloneable {
    void use();
    Shape clone();
}

ConcreatePrototype

Rectangleクラス

public class Rectangle implements Shape {
    private int width, height, x, y;
    private String color;

    public Rectangle(int width, int height, String color, int x, int y) {
        this.width = width;
        this.height = height;
        this.color = color;
        this.x = x;
        this.y = y;
    }

    @Override
    public void use() {
        System.out.println("Rectangle: " + color + " (" + width + "x" + height + ") at (" + x + "," + y + ")");
    }

    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Circleクラス

public class Circle implements Shape {
    private int radius, x, y;
    private String color;

    public Circle(int radius, String color, int x, int y) {
        this.radius = radius;
        this.color = color;
        this.x = x;
        this.y = y;
    }

    @Override
    public void use() {
        System.out.println("Circle: " + color + " (r=" + radius + ") at (" + x + "," + y + ")");
    }

    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Squareクラス

public class Square implements Shape {
    private int size, x, y;
    private String color;

    public Square(int size, String color, int x, int y) {
        this.size = size;
        this.color = color;
        this.x = x;
        this.y = y;
    }

    @Override
    public void use() {
        System.out.println("Square: " + color + " (" + size + "x" + size + ") at (" + x + "," + y + ")");
    }

    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Managerクラス

import java.util.HashMap;
import java.util.Map;

public class Manager {
    private Map<String, Shape> showCase = new HashMap<>();

    public void register(String name, Shape shape) {
        showCase.put(name, shape);
    }

    public Shape create(String name) {
        return showCase.get(name).clone();
    }
}

Mainクラス

public class Main {
    public static void main(String[] args) {
        Manager manager = new Manager();

        manager.register("button", new Rectangle(120, 40, "blue", 0, 0));
        manager.register("circle", new Circle(50, "red", 0, 0));
        manager.register("square", new Square(100, "green", 0, 0));

        Shape btn1 = manager.create("button");
        ((Rectangle) btn1).setPosition(10, 20);
        btn1.use();

        Shape circle1 = manager.create("circle");
        ((Circle) circle1).setPosition(100, 200);
        circle1.use();

        Shape square1 = manager.create("square");
        ((Square) square1).setPosition(50, 150);
        square1.use();
    }
}

メリット

  • 図形の量産: 各図形のインスタンスを一度登録しておけば、clone()を使って必要な分だけ複製できる。位置だけ変更すれば、大量に図形を生成できる。
  • 性能向上: 図形ごとにnewするのではなく、既存のインスタンスを複製して使うため、オブジェクト生成のパフォーマンスが向上する。
  • クラスの整理: 図形が多くなった場合でも、Managerクラスでインスタンスの管理を一元化できる。これにより、個別にクラスを作らなくても済む。

所感

大量のクラスをインスタンス化する必要があり、管理が大変だったり、生成に時間がかかる場合にはPrototypeパターンが非常に有効。ただし、実際の業務ではclone()を使うケースは少ないため、現場で出番があるかは疑問。

raamenwakamaturaamenwakamatu

Builderパターン

概要

複雑なオブジェクト生成処理を分離して、同じ工程で異なる構築結果を得られるようにするパターン。

役割 内容
Product 組み立て対象のクラス
Builder 組み立て手順を定義するインターフェースまたは抽象クラス
ConcreteBuilder Builderを実装し、構築方法を具体的に記述
Director 組み立て手順(順序)を指示する

実装

Product

import java.util.Map;

public class HttpRequest {
    private final String method;
    private final String url;
    private final Map<String, String> headers;
    private final String body;

    public HttpRequest(String method, String url, Map<String, String> headers, String body) {
        this.method = method;
        this.url = url;
        this.headers = headers;
        this.body = body;
    }

    public void send() {
        System.out.println("Sending " + method + " request to " + url);
        System.out.println("Headers: " + headers);
        System.out.println("Body: " + body);
    }
}

Builderインターフェース

public interface HttpRequestBuilder {
    void setMethod();
    void setUrl();
    void setHeaders();
    void setBody();
    HttpRequest build();
}

ConcreteBuilder(POST)

import java.util.HashMap;
import java.util.Map;

public class PostRequestBuilder implements HttpRequestBuilder {
    private String method;
    private String url;
    private Map<String, String> headers = new HashMap<>();
    private String body;

    @Override
    public void setMethod() {
        this.method = "POST";
    }

    @Override
    public void setUrl() {
        this.url = "https://api.example.com/post";
    }

    @Override
    public void setHeaders() {
        headers.put("Content-Type", "application/json");
        headers.put("Authorization", "Bearer TOKEN123");
    }

    @Override
    public void setBody() {
        this.body = "{\"name\":\"test\", \"value\":123}";
    }

    @Override
    public HttpRequest build() {
        return new HttpRequest(method, url, headers, body);
    }
}

ConcreteBuilder(GET)

import java.util.HashMap;
import java.util.Map;

public class GetRequestBuilder implements HttpRequestBuilder {
    private String method;
    private String url;
    private Map<String, String> headers = new HashMap<>();
    private String body;

    @Override
    public void setMethod() {
        this.method = "GET";
    }

    @Override
    public void setUrl() {
        this.url = "https://api.example.com/data?limit=10";
    }

    @Override
    public void setHeaders() {
        headers.put("Accept", "application/json");
        headers.put("Authorization", "Bearer TOKEN123");
    }

    @Override
    public void setBody() {
        this.body = ""; // GETなのでボディは空
    }

    @Override
    public HttpRequest build() {
        return new HttpRequest(method, url, headers, body);
    }
}

Director

public class HttpRequestDirector {
    private final HttpRequestBuilder builder;

    public HttpRequestDirector(HttpRequestBuilder builder) {
        this.builder = builder;
    }

    public HttpRequest construct() {
        builder.setMethod();
        builder.setUrl();
        builder.setHeaders();
        builder.setBody();
        return builder.build();
    }
}

Main(使用例)

public class Main {
    public static void main(String[] args) {
        System.out.println("=== POST Request ===");
        HttpRequestBuilder postBuilder = new PostRequestBuilder();
        HttpRequestDirector postDirector = new HttpRequestDirector(postBuilder);
        HttpRequest postRequest = postDirector.construct();
        postRequest.send();

        System.out.println("\n=== GET Request ===");
        HttpRequestBuilder getBuilder = new GetRequestBuilder();
        HttpRequestDirector getDirector = new HttpRequestDirector(getBuilder);
        HttpRequest getRequest = getDirector.construct();
        getRequest.send();
    }
}

メリット

  • Builderの差し替えだけで、POST/GETなど異なるリクエストが構築できる
  • 新しい種類のリクエストが必要になれば ConcreteBuilder を追加するだけでOK(OCPに準拠)
  • 構築の手順(Director)と詳細実装(ConcreteBuilder)を分離できる

所感

Builderパターンは、構築手順の 共通化(Director) と、構築方法の 差分管理(ConcreteBuilder) ができる点が強い。
テンプレートメソッドと似た思想で、処理の「枠組み」を固定して「中身の実装だけを差し替える」使い方ができる。

今回のようなHTTPリクエストの構築以外にも:

  • SQLクエリ構築
  • HTML DOM構築

など、構築処理が「パーツ化」できて、「共通手順 + 構築差分」に分解できる場面で便利に使える。

raamenwakamaturaamenwakamatu

Abstract Factory パターン

概要

関連するオブジェクト群を、具体的なクラスを指定せずに生成するためのパターン

複数の製品(オブジェクト)がセットとして一貫性を持って動作するように、抽象化されたファクトリを使って生成する。


実装例:暗号方式の切り替え

AbstractProduct

public interface Encryptor {
    String encrypt(String data);
}

public interface Decryptor {
    String decrypt(String data);
}

AbstractFactory

public interface CryptoFactory {
    Encryptor createEncryptor();
    Decryptor createDecryptor();
}

ConcreteProduct

public class AESEncryptor implements Encryptor {
    public String encrypt(String data) {
        return "[AES_ENCRYPTED] " + data;
    }
}

public class AESDecryptor implements Decryptor {
    public String decrypt(String data) {
        return data.replace("[AES_ENCRYPTED] ", "");
    }
}

public class RSAEncryptor implements Encryptor {
    public String encrypt(String data) {
        return "[RSA_ENCRYPTED] " + data;
    }
}

public class RSADecryptor implements Decryptor {
    public String decrypt(String data) {
        return data.replace("[RSA_ENCRYPTED] ", "");
    }
}

ConcreteFactory

public class AESFactory implements CryptoFactory {
    public Encryptor createEncryptor() {
        return new AESEncryptor();
    }

    public Decryptor createDecryptor() {
        return new AESDecryptor();
    }
}

public class RSAFactory implements CryptoFactory {
    public Encryptor createEncryptor() {
        return new RSAEncryptor();
    }

    public Decryptor createDecryptor() {
        return new RSADecryptor();
    }
}

Client

public class CryptoClient {
    private final Encryptor encryptor;
    private final Decryptor decryptor;

    public CryptoClient(CryptoFactory factory) {
        this.encryptor = factory.createEncryptor();
        this.decryptor = factory.createDecryptor();
    }

    public void run(String plainText) {
        String encrypted = encryptor.encrypt(plainText);
        System.out.println("Encrypted: " + encrypted);

        String decrypted = decryptor.decrypt(encrypted);
        System.out.println("Decrypted: " + decrypted);
    }
}

Main

public class Main {
    public static void main(String[] args) {
        System.out.println("=== AES Encryption ===");
        CryptoClient aesClient = new CryptoClient(new AESFactory());
        aesClient.run("Hello World");

        System.out.println("\n=== RSA Encryption ===");
        CryptoClient rsaClient = new CryptoClient(new RSAFactory());
        rsaClient.run("Hello World");
    }
}

メリット

  • CryptoClientどの暗号方式を使っているかを意識しなくていい
  • CryptoFactory を差し替えるだけで 暗号アルゴリズムの切り替えが可能
  • OCP(Open/Closed Principle)に従う:新しい方式(ECCなど)を追加しても、既存コードは変更不要

所感

Builderパターンと同じく「複雑なオブジェクト生成」がテーマだが、目的が異なる:

パターン 目的
Builderパターン 1つの複雑なオブジェクトを段階的に組み立てる
Abstract Factoryパターン 関連する複数の製品群を一括で生成し切り替える

本パターンでは、AESEncryptor, AESDecryptor のように製品同士の整合性が求められる場面に強い。

raamenwakamaturaamenwakamatu

Bridgeパターンとは

概要

抽象(Abstraction)と実装(Implementation)を別々の階層に分けて、独立に変更・拡張できるようにするパターン。


📌 役割と意味

役割 意味 説明
Abstraction 何をするか 出力する、描画する、保存するなどの機能の枠組み
Implementor どうやるか ローカルに保存、ネット経由で送信などの実装手段

実装例:ファイル出力システム

OutputTarget(Implementor)

public interface OutputTarget {
    void write(String filename, String content);
}

実装クラス(ConcreteImplementor)

public class LocalOutput implements OutputTarget {
    public void write(String filename, String content) {
        System.out.println("[LOCAL] Write to " + filename);
        System.out.println("Content: " + content);
    }
}

public class CloudOutput implements OutputTarget {
    public void write(String filename, String content) {
        System.out.println("[CLOUD] Uploading " + filename);
        System.out.println("Content: " + content);
    }
}

ReportWriter(Abstraction)

public abstract class ReportWriter {
    protected final OutputTarget outputTarget;

    public ReportWriter(OutputTarget outputTarget) {
        this.outputTarget = outputTarget;
    }

    protected abstract String buildContent(String data);
    protected abstract String filename();

    public void writeReport(String data) {
        String content = buildContent(data);
        outputTarget.write(filename(), content);
    }
}

機能ごとの具体クラス(RefinedAbstraction)

public class PdfReportWriter extends ReportWriter {
    public PdfReportWriter(OutputTarget outputTarget) {
        super(outputTarget);
    }

    @Override
    protected String buildContent(String data) {
        return "=== PDF ===\n" + data;
    }

    @Override
    protected String filename() {
        return "report.pdf";
    }
}

public class CsvReportWriter extends ReportWriter {
    public CsvReportWriter(OutputTarget outputTarget) {
        super(outputTarget);
    }

    @Override
    protected String buildContent(String data) {
        return "name,age\n" + data;
    }

    @Override
    protected String filename() {
        return "report.csv";
    }
}

クライアントコード

public class Main {
    public static void main(String[] args) {
        ReportWriter pdfLocal = new PdfReportWriter(new LocalOutput());
        pdfLocal.writeReport("Alice, 30");

        ReportWriter csvCloud = new CsvReportWriter(new CloudOutput());
        csvCloud.writeReport("Bob, 35");
    }
}

✅ メリット

1. 機能と実装の分離

  • 実装の詳細を隠蔽して、機能の定義に集中できる
  • 実装の差し替えが容易(例:Cloud → Local)
  • OCP(開放閉鎖原則)に従った設計ができる

2. 独立した拡張が可能

  機能      ×     実装
----------------------------
 PDF     ×  LocalOutput
 PDF     ×  CloudOutput
 CSV        ×  LocalOutput
 CSV        ×  CloudOutput
  • 新しい機能(例:ExcelReportWriter)や、新しい出力先(例:DatabaseOutput)をそれぞれ独立に追加できる。

所感

  • 実装がインターフェースとして注入されるため、DBの切り替えやモックの使用も簡単。
  • 「機能の追加」は RefinedAbstraction に、「出力方法の追加」は ConcreteImplementor に集約できるので、それぞれの拡張ポイントが明確でわかりやすい。
raamenwakamaturaamenwakamatu

ストラテジーパターン

概要

ストラテジーパターン(Strategy Pattern) は、アルゴリズム(処理戦略)を動的に切り替えるためのデザインパターン。利用側(コンテキスト)は処理の具体的な中身を知らずに使うことができる。


実装

Strategy(戦略インターフェース)

public interface PricingStrategy {
    double calculatePrice(double basePrice);     // 料金計算
    double calculateTax(double basePrice);       // 税金計算
    double getDiscountAmount(double basePrice);  // 割引額取得
}

ConcreteStrategy(具体的な戦略)

通常料金戦略

public class RegularPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice) {
        return basePrice;
    }

    @Override
    public double calculateTax(double basePrice) {
        return basePrice * 0.1;
    }

    @Override
    public double getDiscountAmount(double basePrice) {
        return 0.0;
    }
}

割引料金戦略

public class DiscountPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice) {
        return basePrice * 0.9;
    }

    @Override
    public double calculateTax(double basePrice) {
        return (basePrice * 0.9) * 0.1;
    }

    @Override
    public double getDiscountAmount(double basePrice) {
        return basePrice - (basePrice * 0.9);
    }
}

シニア料金戦略

public class SeniorPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice) {
        return basePrice * 0.8;
    }

    @Override
    public double calculateTax(double basePrice) {
        return (basePrice * 0.8) * 0.1;
    }

    @Override
    public double getDiscountAmount(double basePrice) {
        return basePrice - (basePrice * 0.8);
    }
}

学生料金戦略

public class StudentPricingStrategy implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice) {
        return basePrice * 0.85;
    }

    @Override
    public double calculateTax(double basePrice) {
        return (basePrice * 0.85) * 0.1;
    }

    @Override
    public double getDiscountAmount(double basePrice) {
        return basePrice - (basePrice * 0.85);
    }
}

Context(利用クラス)

public class PricingContext {
    private PricingStrategy pricingStrategy;

    public PricingContext(PricingStrategy pricingStrategy) {
        this.pricingStrategy = pricingStrategy;
    }

    public double calculatePrice(double basePrice) {
        return pricingStrategy.calculatePrice(basePrice);
    }

    public double calculateTax(double basePrice) {
        return pricingStrategy.calculateTax(basePrice);
    }

    public double getDiscountAmount(double basePrice) {
        return pricingStrategy.getDiscountAmount(basePrice);
    }

    public void setPricingStrategy(PricingStrategy pricingStrategy) {
        this.pricingStrategy = pricingStrategy;
    }
}

Main(利用例)

public class Main {
    public static void main(String[] args) {
        double basePrice = 1200.0;

        PricingContext context = new PricingContext(new RegularPricingStrategy());
        System.out.println("Regular Price: " + context.calculatePrice(basePrice));
        System.out.println("Tax: " + context.calculateTax(basePrice));

        context.setPricingStrategy(new DiscountPricingStrategy());
        System.out.println("\nDiscount Price: " + context.calculatePrice(basePrice));
        System.out.println("Tax: " + context.calculateTax(basePrice));
        System.out.println("Discount Amount: " + context.getDiscountAmount(basePrice));

        context.setPricingStrategy(new SeniorPricingStrategy());
        System.out.println("\nSenior Price: " + context.calculatePrice(basePrice));
        System.out.println("Tax: " + context.calculateTax(basePrice));
        System.out.println("Discount Amount: " + context.getDiscountAmount(basePrice));

        context.setPricingStrategy(new StudentPricingStrategy());
        System.out.println("\nStudent Price: " + context.calculatePrice(basePrice));
        System.out.println("Tax: " + context.calculateTax(basePrice));
        System.out.println("Discount Amount: " + context.getDiscountAmount(basePrice));
    }
}

メリット

  • 異なる料金戦略(通常、割引、シニア、学生)を追加しても、既存コードに手を加えずに戦略クラスを追加するだけで良い(OCP: 開放・閉鎖の原則)。
  • calculatePrice()calculateTax() などの処理ごとに ifswitch を書く必要がなくなる。
  • ファクトリーメソッドを使って ConcreteStrategy を生成すれば、以降の計算処理では条件分岐を意識せずに済む。
  • 戦略クラスが分離されており、個別にテストしやすい。

所感

ストラテジーパターンは実務でもよく使われるし、分岐を減らせるので保守性の高いコードが書ける。新しい料金種別を追加する際も、具象クラスとファクトリーメソッドの追加だけで対応できるのが嬉しい。

ただし、ストラテジに新しいメソッド(例:料金の最大値など)を追加したい場合、すべての具象クラスに実装が必要になるのが面倒。これはインターフェースベースの設計の宿命ではあるけど、手間なのは確か。

raamenwakamaturaamenwakamatu

Compositeパターン

概要

容器と中身を同一視して、再帰的な構造を作るデザインパターン

実装

Money.java

import java.util.Objects;

public class Money {
    private final int amount;

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

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }

    public int getAmount() {
        return amount;
    }

    @Override
    public String toString() {
        return String.valueOf(amount);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount == money.amount;
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount);
    }
}

Holding.java (Component)

public interface Holding {
    Money getBalance();
}

Transaction.java(Leaf)

public class Transaction implements Holding {
    private final Money money;

    public Transaction(Money money) {
        this.money = money;
    }

    @Override
    public Money getBalance() {
        return money;
    }
}

Account.java(Composite)

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

public class Account implements Holding {
    private final List<Holding> holdings = new ArrayList<>();

    public void add(Holding h) {
        holdings.add(h);
    }

    @Override
    public Money getBalance() {
        Money total = new Money(0);
        for (Holding h : holdings) {
            total = total.add(h.getBalance());
        }
        return total;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Account mainAccount = new Account();
        mainAccount.add(new Transaction(new Money(100)));
        mainAccount.add(new Transaction(new Money(200)));

        Account savings = new Account();
        savings.add(new Transaction(new Money(500)));

        mainAccount.add(savings);

        System.out.println(mainAccount.getBalance());  // 出力: 800
    }
}

メリット

  • 再帰的な構造の取り扱い
    階層構造でも再帰的に扱える。Account の中にさらに Account を入れられる(ネスト可能)。
    getBalance() を呼ぶだけで、再帰的に合計が計算される。

  • 開放閉鎖原則(OCP)に従いやすい
    新しい Holding 実装(例:定期預金や投資商品など)を追加しても、既存のロジックは変更しなくて済む。

所感

再帰的な構造を持ち、かつ容器と中身で同じ操作をしたい場合、呼び出し側からは実装を意識せずにシンプルに使用できる。
getBalance() のような共通のメソッドを使うことで、階層構造を簡単に扱え、拡張性も高く、非常に便利なパターン。

raamenwakamaturaamenwakamatu

Decoratorパターン

概要

Decoratorパターンは、元々の機能(オブジェクト)に対して新しい機能を被せるように追加する構造的デザインパターン。
継承ではなく「委譲」で機能拡張するため、柔軟に振る舞いを追加できるのが特徴。


実装例

Component: サービスインターフェース

public interface Service {
    void execute();
}

ConcreteComponent: 基本的なサービス

public class BasicService implements Service {
    @Override
    public void execute() {
        System.out.println("Basic Service Executed.");
    }
}

Decorator: サービスデコレータ(共通処理)

public abstract class ServiceDecorator implements Service {
    protected final Service service;

    public ServiceDecorator(Service service) {
        this.service = service;
    }

    @Override
    public void execute() {
        service.execute();
    }
}

ConcreteDecorator: 認証機能を追加

public class AuthenticationServiceDecorator extends ServiceDecorator {
    public AuthenticationServiceDecorator(Service service) {
        super(service);
    }

    @Override
    public void execute() {
        if (authenticate()) {
            System.out.println("Authentication successful.");
            super.execute();
        } else {
            System.out.println("Authentication failed.");
        }
    }

    private boolean authenticate() {
        // 仮の認証処理
        System.out.println("Checking authentication...");
        return true;
    }
}

ConcreteDecorator: ログ機能を追加

public class LoggingServiceDecorator extends ServiceDecorator {
    public LoggingServiceDecorator(Service service) {
        super(service);
    }

    @Override
    public void execute() {
        System.out.println("Logging before execution.");
        super.execute();
        System.out.println("Logging after execution.");
    }
}

実行例

public class Main {
    public static void main(String[] args) {
        // 基本のサービス
        Service service = new BasicService();

        // 認証機能を追加
        service = new AuthenticationServiceDecorator(service);

        // ログ機能を追加
        service = new LoggingServiceDecorator(service);

        // 実行
        service.execute();
    }
}

実行結果

Logging before execution.
Checking authentication...
Authentication successful.
Basic Service Executed.
Logging after execution.

メリット

  • 動的に機能追加できる(クラスを書き換えずに)
  • OCP(開放閉鎖原則) に沿った拡張が可能
  • 他のDecoratorと組み合わせて自由に機能構成できる

所感

Decoratorパターンは、特定のケースで基本機能だけでは対応できないときに、既存コードを壊さずに拡張できる点が強み。

例えば、

  • 認証が必要なAPI呼び出し
  • ログ出力を追加したい処理
  • キャッシュ処理やリトライ処理の追加

など、横断的な機能を後から追加したい時に有効。
機能追加の順番も制御できるため、機能の組み合わせの自由度が高い

raamenwakamaturaamenwakamatu

Visitorパターン

概要

Visitorパターンは、オブジェクト構造に対して新しい操作(処理)を後から追加したいときに使うデザインパターン。
Compositeパターンと組み合わせて使われることも多く、構造はそのままで、処理を柔軟に追加したいときに便利。


実装例(Java)

Visitorインターフェース

interface HoldingVisitor {
    void visit(Transaction transaction);
    void visit(Account account);
}

Elementインターフェース

interface Holding {
    void accept(HoldingVisitor visitor);
}

リーフ:取引(Transaction)

class Transaction implements Holding {
    private final Money money;

    public Transaction(Money money) {
        this.money = money;
    }

    @Override
    public void accept(HoldingVisitor visitor) {
        visitor.visit(this);
    }

    public Money getMoney() {
        return money;
    }
}

Composite:アカウント(Account)

class Account implements Holding {
    private final List<Holding> holdings = new ArrayList<>();

    public void add(Holding h) {
        holdings.add(h);
    }

    @Override
    public void accept(HoldingVisitor visitor) {
        visitor.visit(this);
    }

    public List<Holding> getHoldings() {
        return holdings;
    }
}

金額を表すクラス(Money)

class Money {
    private final int amount;

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

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }

    @Override
    public String toString() {
        return String.valueOf(amount);
    }
}

残高を合計するVisitor

class BalanceCalculatorVisitor implements HoldingVisitor {
    private Money total = new Money(0);

    @Override
    public void visit(Account account) {
        for (Holding h : account.getHoldings()) {
            h.accept(this);
        }
    }

    @Override
    public void visit(Transaction transaction) {
        total = total.add(transaction.getMoney());
    }

    public Money getTotal() {
        return total;
    }
}

トランザクションの数を数えるVisitor

class TransactionCountVisitor implements HoldingVisitor {
    private int count = 0;

    @Override
    public void visit(Account account) {
        for (Holding h : account.getHoldings()) {
            h.accept(this);
        }
    }

    @Override
    public void visit(Transaction transaction) {
        count++;
    }

    public int getCount() {
        return count;
    }
}

実行例

public class Main {
    public static void main(String[] args) {
        // データ構築
        Account mainAccount = new Account();
        mainAccount.add(new Transaction(new Money(100)));
        mainAccount.add(new Transaction(new Money(200)));

        Account savings = new Account();
        savings.add(new Transaction(new Money(500)));
        mainAccount.add(savings);

        // 残高計算 Visitor
        BalanceCalculatorVisitor balanceVisitor = new BalanceCalculatorVisitor();
        mainAccount.accept(balanceVisitor);
        System.out.println("Total Balance: " + balanceVisitor.getTotal());  // 800

        // 取引数カウント Visitor
        TransactionCountVisitor countVisitor = new TransactionCountVisitor();
        mainAccount.accept(countVisitor);
        System.out.println("Total Transactions: " + countVisitor.getCount());  // 3
    }
}

メリット

  • 構造クラスの変更なしで、新しい処理を外部に追加できる
  • 共通の処理(走査など)をまとめて管理できる
  • Composite構造との相性が良い

所感

Compositeパターンでは構造クラス内に処理を持たせていたが、Visitorパターンにより処理の責務を外出しにできるため、柔軟に拡張できると感じた。しかし、新しい構造クラスを追加するとすべてのVisitorにvisitメソッドを追加する必要があるので、 構造がほとんど変わらず、処理だけ増えていくようなシステムに有効だと感じた。

raamenwakamaturaamenwakamatu

Chain of Responsibilityパターン

概要

リクエストを処理できるオブジェクトをチェーン上に並べ、順番に処理を試みる構造。
条件にマッチした時点で処理を打ち切ったり、次の処理に渡したりできる。


実装例

Handler

abstract class Validator {
    private Validator next;

    public Validator setNext(Validator next) {
        this.next = next;
        return next;
    }

    public final boolean validate(String input) {
        if (!check(input)) {
            return false;
        }
        if (next != null) {
            return next.validate(input);
        }
        return true;
    }

    protected abstract boolean check(String input);
}

ConcreateHandler

class NotEmptyValidator extends Validator {
    @Override
    protected boolean check(String input) {
        if (input == null || input.isEmpty()) {
            System.out.println("Validation failed: input is empty.");
            return false;
        }
        return true;
    }
}

class LengthValidator extends Validator {
    private final int max;

    public LengthValidator(int max) {
        this.max = max;
    }

    @Override
    protected boolean check(String input) {
        if (input.length() > max) {
            System.out.println("Validation failed: input is too long.");
            return false;
        }
        return true;
    }
}

class EmailValidator extends Validator {
    @Override
    protected boolean check(String input) {
        if (!input.contains("@")) {
            System.out.println("Validation failed: not a valid email.");
            return false;
        }
        return true;
    }
}

Main

public class Main {
    public static void main(String[] args) {
        Validator validator = new NotEmptyValidator();
        validator
            .setNext(new LengthValidator(20))
            .setNext(new EmailValidator());

        String input = "example@example.com";
        boolean result = validator.validate(input);
        System.out.println("Validation result: " + result);
    }
}

✅ 実行結果

Validation result: true

不正な入力 "toolongemailaddress@example.com" にした場合:

Validation failed: input is too long.
Validation result: false

メリット

  • 単一責任の原則を守れる(バリデーションごとにクラスを分ける)
  • チェーン構成の変更が簡単(追加・削除・順序変更が容易)

所感

バリデーションのように、複数条件を順番にチェックしていく処理には非常に向いている。
また、条件を追加する場合も新しい具象 Validator を実装するだけでよく、既存のクラスを変更せずに拡張できるのが良い。

1点気になったのは、setNext メソッドがチェーンの構築と次のインスタンスの返却を兼ねており、コマンドとクエリの分離原則(CQS) には反していること。

raamenwakamaturaamenwakamatu

Facadeパターン

概要

複雑な処理や複数のクラスのやり取りをシンプルな窓口(Facade)で隠蔽して使いやすくするパターン。
利用者は内部の詳細を知らずに、Facadeを通じて簡潔に機能を使えるようになる。


実装例:ログイン処理

下位レベルのクラスたち

class AuthService {
    public boolean authenticate(String username, String password) {
        System.out.println("認証中: " + username);
        return "user".equals(username) && "pass".equals(password);
    }
}

class SessionManager {
    public void createSession(String username) {
        System.out.println("セッション作成: " + username);
    }
}

class Logger {
    public void log(String message) {
        System.out.println("ログ: " + message);
    }
}

Facadeクラス

class LoginFacade {
    private final AuthService authService = new AuthService();
    private final SessionManager sessionManager = new SessionManager();
    private final Logger logger = new Logger();

    public boolean login(String username, String password) {
        logger.log("ログイン試行: " + username);
        if (authService.authenticate(username, password)) {
            sessionManager.createSession(username);
            logger.log("ログイン成功: " + username);
            return true;
        } else {
            logger.log("ログイン失敗: " + username);
            return false;
        }
    }
}

Mainクラス

public class Main {
    public static void main(String[] args) {
        LoginFacade login = new LoginFacade();
        boolean success = login.login("user", "pass");
        System.out.println("ログイン結果: " + success);
    }
}

✅ メリット

  • クライアント側が複数のクラスを意識せずに使える

所感

  • Main から呼び出す全ての処理を 1つの巨大な Facade にまとめるのは危険
  • Use Case ごとに Facade を分割すれば、役割が明確になる
  • Facade 自体も「単一責任の原則(SRP)」を意識して設計すべき
raamenwakamaturaamenwakamatu

Mediatorパターン

概要

「オブジェクト同士の直接のやり取りを避け、仲介役のオブジェクトを介してやり取りさせる」ことで、依存関係を減らし、疎結合にするデザインパターン。

実装例

Colleagueインターフェース

interface Colleague {
    void send(String message);
    void receive(String message);
    String getName();
}

Userクラス(具体的なColleague)

class User implements Colleague {
    private final String name;
    private final ChatRoom mediator;

    public User(String name, ChatRoom mediator) {
        this.name = name;
        this.mediator = mediator;
    }

    @Override
    public void send(String message) {
        mediator.sendMessage(message, this);
    }

    @Override
    public void receive(String message) {
        System.out.println(name + " received: " + message);
    }

    @Override
    public String getName() {
        return name;
    }
}

ChatRoom(Medaitor)

interface ChatRoom {
    void sendMessage(String message, Colleague sender);
    void registerUser(Colleague user);
}

ChatRoomの実装(ConcreateMedaitor)

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

class ChatRoomImpl implements ChatRoom {
    private final List<Colleague> users = new ArrayList<>();

    @Override
    public void sendMessage(String message, Colleague sender) {
        for (Colleague user : users) {
            if (!user.equals(sender)) {
                user.receive(sender.getName() + ": " + message);
            }
        }
    }

    @Override
    public void registerUser(Colleague user) {
        users.add(user);
    }
}

Mainクラス

public class Main {
    public static void main(String[] args) {
        ChatRoomImpl chatRoom = new ChatRoomImpl();

        Colleague alice = new User("Alice", chatRoom);
        Colleague bob = new User("Bob", chatRoom);
        Colleague carol = new User("Carol", chatRoom);

        chatRoom.registerUser(alice);
        chatRoom.registerUser(bob);
        chatRoom.registerUser(carol);

        alice.send("やあ、みんな!");
        bob.send("こんにちは Alice!");
    }
}

実行結果

Bob received: Alice: やあ、みんな!
Carol received: Alice: やあ、みんな!
Alice received: Bob: こんにちは Alice!
Carol received: Bob: こんにちは Alice!

メリット

  • Colleague同士の依存関係がなくなり、複雑さが軽減される。
  • 具象Colleagueの追加が比較的容易(匿名ユーザーなど)。

所感

  • UI部品やチャット機能に有効。
  • ただし、責務が集中しすぎると、Facadeと似たように重くなるため、適度な分割と機能整理が必要。
raamenwakamaturaamenwakamatu

Observerパターン

概要

Observerパターンは、あるオブジェクト(Subject)の状態が変更された際に、関連するオブジェクト(Observer)に通知を行う仕組みだ。このパターンを使うことで、処理後に関連するアクション(例えば、メール通知やログ保存)を簡単に実行できる。

実装例

Subject

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

// RequestMonitor (抽象クラス)
abstract class RequestMonitor {
    private List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    // 不正リクエストを処理するための抽象メソッド
    public abstract void execute(Request request);

    // Observerに通知する
    protected void notifyObservers(SuspiciousRequestEvent event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

ConcreteSubject

// SuspiciousRequestMonitor (不正リクエストを監視する具象クラス)
class SuspiciousRequestMonitor extends RequestMonitor {

    @Override
    public void execute(Request request) {
        // 不正リクエストかどうかをチェック
        if (isSuspicious(request)) {
            SuspiciousRequestEvent event = new SuspiciousRequestEvent(request);
            notifyObservers(event);  // 不正リクエストが検出された場合に通知を行う
        }
    }

    // 不正リクエストかどうかを判定するメソッド
    private boolean isSuspicious(Request request) {
        // 例: 認証ヘッダーがない場合
        return request.getAuthorizationHeader() == null || request.getAuthorizationHeader().isEmpty();
    }
}

Observer

// Observer (通知を受け取るインターフェース)
interface Observer {
    void update(SuspiciousRequestEvent event);
}

ConcreteObserver

// EmailObserver (不正リクエストがあった場合にメールを送信する)
class EmailObserver implements Observer {

    @Override
    public void update(SuspiciousRequestEvent event) {
        sendEmail(event);
    }

    private void sendEmail(SuspiciousRequestEvent event) {
        System.out.println("Sending email: Suspicious request detected at " + event.getTimestamp() +
                ". Request details: " + event.getRequest());
        // 実際のメール送信ロジック(JavaMailなど)をここで記述
    }
}

import java.util.logging.Logger;

// LogObserver (不正リクエストのログ記録を行う)
class LogObserver implements Observer {
    private static final Logger logger = Logger.getLogger(LogObserver.class.getName());

    @Override
    public void update(SuspiciousRequestEvent event) {
        logRequest(event);
    }

    private void logRequest(SuspiciousRequestEvent event) {
        logger.info("Suspicious request detected at " + event.getTimestamp() +
                ". Request details: " + event.getRequest());
    }
}

Request

// Request (リクエストの詳細を格納)
class Request {
    private final String authorizationHeader;

    public Request(String authorizationHeader) {
        this.authorizationHeader = authorizationHeader;
    }

    public String getAuthorizationHeader() {
        return authorizationHeader;
    }

    @Override
    public String toString() {
        return "Authorization Header: " + authorizationHeader;
    }
}

SuspiciousRequestEvent

import java.util.Date;

// SuspiciousRequestEvent (不正リクエストの情報を格納)
class SuspiciousRequestEvent {
    private final Request request;
    private final Date timestamp;

    public SuspiciousRequestEvent(Request request) {
        this.request = request;
        this.timestamp = new Date();
    }

    public Request getRequest() {
        return request;
    }

    public Date getTimestamp() {
        return timestamp;
    }
}

実行

public class Main {
    public static void main(String[] args) {
        // RequestMonitorとObserverをセットアップ
        SuspiciousRequestMonitor requestMonitor = new SuspiciousRequestMonitor();
        EmailObserver emailObserver = new EmailObserver();
        LogObserver logObserver = new LogObserver();

        // ObserverをRequestMonitorに追加
        requestMonitor.addObserver(emailObserver);
        requestMonitor.addObserver(logObserver);  // ログ記録のObserverを追加

        // シミュレーション: 不正リクエストを処理
        Request request = new Request(null); // 認証ヘッダーが無い場合
        requestMonitor.execute(request);  // 不正リクエストイベントを発行
    }
}

メリット

  • 新しくObserverを追加するだけで、メール通知やログ保存に加えて、外部の監視システムの通知などを追加することが容易。
  • 新しくSubjectを追加するだけで、認証ヘッダーがない場合以外にもリクエストのバリデーション処理などを追加することができる。

所感

本書の例など、Observerパターンを使用してUIの更新を行う例が多いけど、フロントエンドではフレームワークのバインディングの機能が利用されることが多いため、一から実装する機会は少ないかもしれない。
不正リクエストに対する通知処理(メール送信やログ記録)のようなケースでは、このパターンはすごく有用に思った。Observerパターンを使うことで、新しい通知手段や処理を簡単に追加でき、柔軟で保守しやすいシステムが作れる。

raamenwakamaturaamenwakamatu

Mementoパターン

概要

Mementoパターンは、現在のオブジェクトの状態を記録し、後でその状態に戻すことができるデザインパターン

実装例

Memento.ts

export class Memento {
  private static readonly KEY = 'formState'; // formStateのキー名を保持
  private formData: { name: string; email: string };

  constructor(formData: { name: string; email: string }) {
    this.formData = formData;
  }

  // 状態の取得
  getFormData(): { name: string; email: string } {
    return this.formData;
  }

  // ストレージに保存
  static save(memento: Memento): void {
    localStorage.setItem(Memento.KEY, JSON.stringify(memento.getFormData()));
  }

  // ストレージから状態をリストア
  static restore(): Memento | null {
    const savedState = localStorage.getItem(Memento.KEY);
    if (savedState) {
      const formData = JSON.parse(savedState);
      return new Memento(formData);
    }
    return null;
  }
}

FormOriginator.ts

export class FormOriginator {
  private formData: { name: string; email: string };

  constructor(initialState: { name: string; email: string }) {
    this.formData = initialState;
  }

  // 状態のスナップショットを作成
  createMemento(): Memento {
    return new Memento(this.formData);
  }

  // 状態を復元
  restoreMemento(memento: Memento): void {
    this.formData = memento.getFormData();
  }

  // 現在の状態を取得
  getState(): { name: string; email: string } {
    return this.formData;
  }
}

Main.tsx

import React, { useState } from 'react';
import { FormOriginator } from './FormOriginator';
import { Memento } from './Memento';

const Main = () => {
  const [formData, setFormData] = useState({ name: '', email: '' });

  // Originatorインスタンス化
  const originator = new FormOriginator(formData);

  // 保存ボタンをクリックした時
  const handleSave = () => {
    const memento = originator.createMemento();
    Memento.save(memento); // Mementoを保存
    console.log('Form state saved:', memento);
  };

  // 復元ボタンをクリックした時
  const handleRestore = () => {
    const memento = Memento.restore(); // Mementoを取得
    if (memento) {
      originator.restoreMemento(memento); // Mementoを復元
      setFormData(originator.getState()); // フォームの状態を更新
      console.log('Form state restored:', memento);
    } else {
      console.log('No saved state found');
    }
  };

  // フォームの変更
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  };

  return (
    <div>
      <h1>Form with Memento Pattern</h1>
      <form>
        <div>
          <label>Name: </label>
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleChange}
          />
        </div>
        <div>
          <label>Email: </label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
          />
        </div>
      </form>
      <div>
        <button onClick={handleSave}>Save</button>
        <button onClick={handleRestore}>Restore</button>
      </div>
    </div>
  );
};

export default Main;

メリット

  • オブジェクトの状態を保存しておき、後でその状態に戻すことができる。
  • フォームの一時保存と復元を簡単に管理できる。

所感

CaretakerMain.tsx)と Originator を分ける目的は少しわかりにくいと感じたが、Originator は状態の作成と復元を担当し、Caretaker は保存や復元のタイミングを決定する責任を持っているらしい(この例ではボタンが押下された時)。将来的に保存方法を変更したい場合(例えば、ローカルストレージや Cookie など)、Main.tsxCaretaker)のコードはそのままで、新しい保存方法を追加するだけで済むというメリットはある。

raamenwakamaturaamenwakamatu

Stateパターン

概要

Stateパターンは、オブジェクトの内部状態によって振る舞いを変えるためのデザインパターン。状態をクラスとして定義することで、状態遷移とそれに伴う挙動をわかりやすく分離できる。


実装例(Java)

状態インターフェース

interface RequestState {
    void submit(Request request);
    void approve(Request request);
    void reject(Request request);
    void printStatus();
}

申請中状態

class DraftState implements RequestState {
    @Override
    public void submit(Request request) {
        System.out.println("申請を提出しました。");
        request.setState(new SubmittedState());
    }

    @Override
    public void approve(Request request) {
        System.out.println("申請中状態から直接承認することはできません。");
    }

    @Override
    public void reject(Request request) {
        System.out.println("申請中状態から直接差し戻すことはできません。");
    }

    @Override
    public void printStatus() {
        System.out.println("申請ステータス: 申請中");
    }
}

申請済み状態

class SubmittedState implements RequestState {
    @Override
    public void submit(Request request) {
        System.out.println("すでに申請済みです。");
    }

    @Override
    public void approve(Request request) {
        System.out.println("申請が承認されました。");
        request.setState(new ApprovedState());
    }

    @Override
    public void reject(Request request) {
        System.out.println("申請が差し戻されました。");
        request.setState(new RejectedState());
    }

    @Override
    public void printStatus() {
        System.out.println("申請ステータス: 申請済み");
    }
}

承認済み状態

class ApprovedState implements RequestState {
    @Override
    public void submit(Request request) {
        System.out.println("すでに承認済みの申請を再提出することはできません。");
    }

    @Override
    public void approve(Request request) {
        System.out.println("すでに承認されています。");
    }

    @Override
    public void reject(Request request) {
        System.out.println("承認済みの申請を差し戻すことはできません。");
    }

    @Override
    public void printStatus() {
        System.out.println("申請ステータス: 承認済み");
    }
}

差し戻し状態

class RejectedState implements RequestState {
    @Override
    public void submit(Request request) {
        System.out.println("差し戻された申請を再度提出できます。");
    }

    @Override
    public void approve(Request request) {
        System.out.println("差し戻された状態から直接承認することはできません。");
    }

    @Override
    public void reject(Request request) {
        System.out.println("すでに差し戻されています。");
    }

    @Override
    public void printStatus() {
        System.out.println("申請ステータス: 差し戻し");
    }
}

リクエスト本体(Context)

class Request {
    private RequestState state;

    public Request() {
        this.state = new DraftState(); // 初期状態
    }

    public void setState(RequestState state) {
        this.state = state;
    }

    public void submit() {
        state.submit(this);
    }

    public void approve() {
        state.approve(this);
    }

    public void reject() {
        state.reject(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

使用例

public class StatePatternExample {
    public static void main(String[] args) {
        Request request = new Request();
        request.printStatus(); // 申請ステータス: 申請中

        request.submit();      // 申請を提出しました。
        request.printStatus(); // 申請ステータス: 申請済み

        request.approve();     // 申請が承認されました。
        request.printStatus(); // 申請ステータス: 承認済み

        request.reject();      // 承認済みの申請を差し戻すことはできません。
    }
}

メリット

  • 分岐が減る
    通常は if (state == DRAFT) のような分岐が大量に出てくるが、それを各状態クラスに分離できる。

  • 状態追加の影響が小さい
    新しい状態が必要になっても、既存クラスの修正は最小限。状態クラスを追加するのと
    新しく追加した状態を知っているクラスの修正で済む。


所感

小規模な状態(例:2パターン)であればそこまで恩恵はない。ただ、今後状態が増える可能性があるなら、最初からStateパターンで組んでおいた方が影響を最小限にできる。
状態遷移の責任も重要。Context(この例では Request)に全状態を把握させると肥大化しやすく、状態追加時の影響が大きい。一方で各 ConcreteState に「次の状態だけを知っておく」ようにすれば、状態間の依存も限定的になるため、保守しやすい設計になる。

raamenwakamaturaamenwakamatu

Flyweightパターン

概要

Flyweightパターンは、「同じようなオブジェクトを大量に生成するのを避けたいとき」に使えるデザインパターン。

目的は「インスタンスの再利用によるメモリ節約」
大量のデータが同じ内部状態(intrinsic state)を持っている場合、それを共有することで無駄な new を減らす。


実装例(Java)

import java.util.HashMap;
import java.util.Map;

// フライウェイトインターフェース
interface Flyweight {
    void operation(String extrinsicState);
}

重いオブジェクト(ConcreteFlyweight)

class ConcreteFlyweight implements Flyweight {
    private final String intrinsicState;

    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String extrinsicState) {
        System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);
    }
}

Flyweight Factory(インスタンス管理)

class FlyweightFactory {
    private final Map<String, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String intrinsicState) {
        if (!flyweights.containsKey(intrinsicState)) {
            flyweights.put(intrinsicState, new ConcreteFlyweight(intrinsicState));
        }
        return flyweights.get(intrinsicState);
    }
}

使用例

public class FlyweightExample {
    public static void main(String[] args) {
        FlyweightFactory factory = new FlyweightFactory();

        Flyweight flyweight1 = factory.getFlyweight("A");
        Flyweight flyweight2 = factory.getFlyweight("A");
        Flyweight flyweight3 = factory.getFlyweight("B");

        flyweight1.operation("first");   // Intrinsic: A, Extrinsic: first
        flyweight2.operation("second");  // Intrinsic: A, Extrinsic: second
        flyweight3.operation("third");   // Intrinsic: B, Extrinsic: third
    }
}

メリット

  • メモリ節約
    同じ内部状態のインスタンスを共有することで、無駄なオブジェクト生成を防げる。
  • 生成コスト削減
    new の回数が減るので、生成処理のコストも抑えられる。

所感

グラフィックやテキストのレンダリングなど、同じ形状や文字が繰り返し使用され、共有可能な状態が多いケースでは有効なパターン。ただし、現代的なWebアプリケーション開発においては、利用シーンはやや限定的で、実装機会は少ない印象がある。

✅ Flyweightパターンと値オブジェクトの設計思想の違い

たとえばドメイン駆動設計(DDD)でよく登場する「値オブジェクト(Value Object)」は、メソッドを実行するたびに新しいインスタンスを返す。これにより不変性(immutability) を維持し、意図しない状態変化を防ぐ。

一方で、Flyweightパターンは「同じ内部状態ならインスタンスは共有して使おう」というアプローチで、これはメモリ節約やオブジェクト生成コストの低減を主眼に置いた設計となる。

観点 Flyweight 値オブジェクト
主目的 メモリ効率 不変性と設計の安全性
状態の共有 する(再利用) しない(毎回生成)
newの頻度 減らす 増える
使用シーン UI描画(フォント・パーティクル)など 金額、ユーザー名、識別子など

状態を安全に扱いたいなら値オブジェクト、リソース効率を重視したいならFlyweight、といったように設計意図によって使い分ける必要がある。

raamenwakamaturaamenwakamatu

Proxyパターン

概要

本人(RealSubject)をインスタンス化するのに時間がかかるなど重い処理がある場合、
代理人(Proxy)が一部の仕事を代行しつつ、必要になるまで本人のインスタンス化を遅延させるパターン。


実装例(TypeScript)

// Subjectインターフェース
interface API {
  fetchData(): Promise<any>;
}

// RealSubject(本物)
class RealAPI implements API {
  fetchData(): Promise<any> {
    console.log('RealAPI: データ取得処理中...');
    return fetch('/data').then(res => res.json());
  }
}

// Proxyクラス
class APIProxy implements API {
  private realAPI: RealAPI | null = null;
  private cache: any = null;

  // ユーザー権限のダミーチェック(ここは任意実装)
  private hasPermission(): boolean {
    // 例: ログインしていなければfalse返す想定
    return true; // 実際の判定ロジックを入れる
  }

  fetchData(): Promise<any> {
    if (!this.hasPermission()) {
      return Promise.reject(new Error('アクセス権限がありません'));
    }

    if (this.cache) {
      console.log('APIProxy: キャッシュから返却');
      return Promise.resolve(this.cache);
    }

    if (!this.realAPI) {
      console.log('APIProxy: RealAPIのインスタンスを生成');
      this.realAPI = new RealAPI();
    }

    console.log('APIProxy: RealAPI.fetchDataを呼び出し');
    return this.realAPI.fetchData().then(data => {
      console.log('APIProxy: データをキャッシュに保存');
      this.cache = data;
      return data;
    });
  }
}

// 使用例
const api: API = new APIProxy();

api.fetchData()
  .then(data => console.log('1回目の取得:', data))
  .catch(err => console.error(err));

api.fetchData()
  .then(data => console.log('2回目の取得:', data))
  .catch(err => console.error(err));

メリット

  • 権限がない場合やキャッシュがある場合は、重いRealAPIのインスタンス化を回避できる。
  • 必要なタイミングでのみRealAPIを初期化し、無駄な処理を減らせる。

所感

ストラテジパターンのように複数のロジックを切り替える用途は基本的にないのでSubject(共通インターフェース)を用意するメリットはテスト用モック作成や将来的な差し替えの容易さくらいなのかなと感じた。また、本人のインスタンス生成に時間がかかるときなどに有効なので実務であまり出番が少ないパターンなのかなと感じた。

raamenwakamaturaamenwakamatu

Commandパターン

概要

Commandパターンは「命令をクラスで表現する」パターン。
実行したい処理をオブジェクトとして扱い、呼び出し側と処理側を疎結合にできる。

主な目的は以下の通り:

  • 呼び出しと実行の分離(Invokerは処理の中身を知らない)
  • 履歴管理による undo / redo の実装
  • コマンドの再利用・スケジュール・キュー投入などの柔軟性

構成

  • Command:命令の共通インターフェース
  • ConcreteCommand:具体的な命令の実装
  • Receiver:実際の処理を行う対象
  • Invoker:コマンドを実行・管理する呼び出し役

実装例(電気のON/OFF操作)

// Command: コマンドのインターフェース
interface Command {
  execute(): void;
  undo(): void;
}

// Receiver: 実際の操作対象
class Light {
  private isOn = false;

  turnOn() {
    this.isOn = true;
    console.log('電気をつけた');
  }

  turnOff() {
    this.isOn = false;
    console.log('電気を消した');
  }
}

// ConcreteCommand: 電気をつける命令
class LightOnCommand implements Command {
  constructor(private light: Light) {}

  execute() {
    this.light.turnOn();
  }

  undo() {
    this.light.turnOff();
  }
}

// ConcreteCommand: 電気を消す命令
class LightOffCommand implements Command {
  constructor(private light: Light) {}

  execute() {
    this.light.turnOff();
  }

  undo() {
    this.light.turnOn();
  }
}

// Invoker: コマンドの実行と履歴管理
class RemoteControl {
  private history: Command[] = [];

  executeCommand(command: Command) {
    command.execute();
    this.history.push(command);
  }

  undoLast() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    } else {
      console.log('Undoできる操作がありません');
    }
  }
}

使用例(クライアントコード)

const light = new Light();                          // Receiver
const lightOn = new LightOnCommand(light);          // ConcreteCommand
const lightOff = new LightOffCommand(light);        // ConcreteCommand
const remote = new RemoteControl();                 // Invoker

remote.executeCommand(lightOn);    // => 電気をつけた
remote.executeCommand(lightOff);   // => 電気を消した

remote.undoLast(); // => 電気をつけた
remote.undoLast(); // => 電気を消した

✅ メリット

  • 疎結合:Invokerは具体的な処理内容を知らなくて良い
  • 拡張性:新しいコマンド追加時、InvokerやReceiverの修正が不要
  • 履歴管理:操作履歴を保持すれば undo / redo を簡単に実装可能
  • 柔軟性:マクロコマンド(複数コマンドをまとめて実行)や非同期実行にも応用可能

所感

ユーザ操作の履歴が重要なアプリ(テキストエディタ、画像編集アプリ、ゲームのリプレイ機能など)に向いてる。特に undo / redo が必要ならより有効。

また、Invokerはコマンドの詳細を知らなくていいので、今回のように LightOnCommandLightOffCommand が2パターンしかなくても、将来的にコマンドが増えても 既存コードを変更せずに拡張できる というのが大きな魅力。

raamenwakamaturaamenwakamatu

Interpreterパターン

概要

特定の言語の文法をクラスで表現し、文を解釈(評価)する」ためのデザインパターン。