Javaを鍛える|デザインパターン全23回 #3

Factory Method:チケット種別ごとの生成分離

  • Java
  • デザインパターン
  • Factory Method
  • Javaを鍛える

📋 この記事の目次
  1. Factory Methodは何のためにあるか
  2. Before:生成ロジックが呼び出し側に直接書かれている
  3. After:生成ロジックをFactoryクラスに集約する
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftでは、Webフォームからのチケット登録画面と、バッチ取り込み処理の両方に、次のような「種類ごとの生成分岐」が個別に書かれています。

public class TicketController {
    public Ticket handleNewTicketRequest(String type, String title, String detail) {
        Ticket ticket;
        if (type.equals("bug")) {
            ticket = new BugTicket("T-" + System.nanoTime(), title, "medium");
        } else if (type.equals("feature")) {
            ticket = new FeatureTicket("T-" + System.nanoTime(), title, detail);
        } else if (type.equals("inquiry")) {
            ticket = new InquiryTicket("T-" + System.nanoTime(), title);
        } else {
            throw new IllegalArgumentException("未知のチケット種別: " + type);
        }
        return ticket;
    }
}

新しいチケット種別(例えば「改善提案」)を1つ増やすたびに、この分岐をWebフォーム側・バッチ取り込み側の両方に追加しなければなりません。追加漏れが起きるのは時間の問題です。

Factory Methodは何のためにあるか

Factory Methodは、「オブジェクトの生成方法を1箇所にまとめ、呼び出し側は『何が欲しいか』だけを伝えればよい状態にする」 パターンです。社内の備品を、各部署が自分で発注するのではなく、総務部の窓口に「これが欲しい」と伝えるだけで届けてもらえるようにする、というイメージです。

Before:生成ロジックが呼び出し側に直接書かれている

public class Main {
    public static void main(String[] args) {
        TicketController controller = new TicketController();
        Ticket t1 = controller.handleNewTicketRequest("bug", "ログイン画面が真っ白になる", "");
        Ticket t2 = controller.handleNewTicketRequest("feature", "ダークモードが欲しい", "山田花子");
        Ticket t3 = controller.handleNewTicketRequest("inquiry", "請求書の発行方法について", "");
        System.out.println(t1.summary());
        System.out.println(t2.summary());
        System.out.println(t3.summary());
    }
}

実行結果:

[バグ] ログイン画面が真っ白になる(重要度: medium)
[要望] ダークモードが欲しい(提案者: 山田花子)
[問い合わせ] 請求書の発行方法について

正しく動いてはいますが、TicketController自身が「どの種類のときに、どのクラスを、どう組み立てるか」まで知ってしまっています。同じ分岐が別のクラス(バッチ取り込み処理など)にもコピーされていれば、修正のたびにすべてを直す必要があります。

After:生成ロジックをFactoryクラスに集約する

public class TicketFactory {
    public Ticket createTicket(String type, String title, String detail) {
        switch (type) {
            case "bug":
                return new BugTicket("T-" + System.nanoTime(), title, "medium");
            case "feature":
                return new FeatureTicket("T-" + System.nanoTime(), title, detail);
            case "inquiry":
                return new InquiryTicket("T-" + System.nanoTime(), title);
            default:
                throw new IllegalArgumentException("未知のチケット種別: " + type);
        }
    }
}
public class TicketController {
    private final TicketFactory factory = new TicketFactory();

    public Ticket handleNewTicketRequest(String type, String title, String detail) {
        return factory.createTicket(type, title, detail);
    }
}

Mainクラス側は変更なしで、実行結果もBeforeとまったく同じです。

[バグ] ログイン画面が真っ白になる(重要度: medium)
[要望] ダークモードが欲しい(提案者: 山田花子)
[問い合わせ] 請求書の発行方法について

ポイントは、振る舞いを変えずに、生成の責任だけをTicketFactoryという1つの窓口に移したことです。バッチ取り込み処理も同じTicketFactoryを呼び出すようにすれば、新しいチケット種別を追加するときの修正箇所はTicketFactoryの1箇所だけになります。

🧓 ベテランの現場コラム Factory Methodは「単純にnewを1回別のメソッドに移しただけ」に見えるかもしれませんが、効果は絶大です。現場で「この種類を追加するのに、なぜこんなに何箇所も直す必要があるのか」と感じたら、まず疑うべきはこの生成ロジックの重複です。逆に、生成方法が本当に1箇所でしか使われていない場合にまでFactoryクラスを作ると、無駄なクラスが増えるだけになるので注意してください。

※厳密なGoFのFactory Methodは、生成用のメソッドを持つ抽象クラスを作り、サブクラスに生成を委譲する設計を指します。今回のTicketFactoryのようにswitch文で分岐する形は、現場では「Simple Factory(単純ファクトリ)」と呼ばれることが多い派生形です。とはいえ実務で「生成ロジックを1箇所に集約する」という目的で使われるのは、圧倒的にこちらの形です。

こんな場面で使う/使わない

  • 使う: 同じ種類分岐によるオブジェクト生成が、複数の呼び出し元にまたがって存在する(または存在しそうな)場合
  • 使わない: オブジェクトの生成方法が単純で、呼び出し元も1箇所しかない場合。無理にFactoryクラスを作る必要はない

まとめ

  • Factory Methodは「生成の責任を1つの窓口にまとめる」パターン
  • 種類ごとの分岐が複数箇所に重複しているコードで特に効果を発揮する
  • 呼び出し側のコード・実行結果は変えず、内部の設計だけを整理できる

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。