java-recipes

ホーム デザインパターン › DP-03

DP-03: Factory Method パターン

オブジェクトの生成処理をサブクラスに委譲するパターンです。Collections.emptyList()Files.newBufferedReader() など、 Java 標準ライブラリでも広く使われています。

Factory Method パターンとは

Factory Method パターンは「オブジェクトを生成するためのインターフェースを定義しつつ、 どのクラスをインスタンス化するかはサブクラスが決める」パターンです。 親クラス(抽象ファクトリー)は「生成する」という流れだけを定義し、 具体的に何を生成するかはサブクラスに委ねます。

Simple Factory との違い

項目Simple FactoryFactory Method
生成処理の場所1つのクラスのメソッド(if-else で分岐)サブクラスのオーバーライドメソッド
拡張方法既存クラスを修正する新しいサブクラスを追加する
GoF パターンGoF 外(非パターン)GoF の 23 パターンに含まれる

Java 標準ライブラリの実例として、Collections.emptyList()(空の不変リスト)、Collections.singletonList()(1要素の不変リスト)、java.nio.file.Files.newBufferedReader()(ファイルを読み取る Reader の生成) などが Factory Method の考え方を採用しています。 呼び出し側は List や Reader といったインターフェース型だけを知れば足ります。

サンプルコード

FactoryMethodSample.java
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class FactoryMethodSample {

    // レポートの共通インターフェース(Product)
    interface Report {
        String generate(String title, String content);
    }

    // PDF 形式のレポート(ConcreteProduct)
    static class PdfReport implements Report {
        @Override
        public String generate(String title, String content) {
            return "[PDF] タイトル: " + title + " | 内容: " + content + " | 形式: Adobe PDF";
        }
    }

    // HTML 形式のレポート(ConcreteProduct)
    static class HtmlReport implements Report {
        @Override
        public String generate(String title, String content) {
            return "<html><head><title>" + title + "</title></head>"
                + "<body><p>" + content + "</p></body></html>";
        }
    }

    // CSV 形式のレポート(ConcreteProduct)
    static class CsvReport implements Report {
        @Override
        public String generate(String title, String content) {
            return "\"" + title + "\",\"" + content + "\"";
        }
    }

    // ファクトリーの抽象クラス(Creator)
    // createReport() が Factory Method です
    static abstract class ReportFactory {

        // Factory Method: サブクラスが具体的な生成処理を実装します
        protected abstract Report createReport();

        // テンプレートメソッド: 生成から出力までの流れを定義
        public String process(String title, String content) {
            Report report = createReport(); // サブクラスの Factory Method を呼び出す
            return report.generate(title, content);
        }
    }

    // PDF レポートを生成するファクトリー(ConcreteCreator)
    static class PdfReportFactory extends ReportFactory {
        @Override
        protected Report createReport() {
            return new PdfReport();
        }
    }

    // HTML レポートを生成するファクトリー(ConcreteCreator)
    static class HtmlReportFactory extends ReportFactory {
        @Override
        protected Report createReport() {
            return new HtmlReport();
        }
    }

    // CSV レポートを生成するファクトリー(ConcreteCreator)
    static class CsvReportFactory extends ReportFactory {
        @Override
        protected Report createReport() {
            return new CsvReport();
        }
    }

    // Java 標準ライブラリの Factory Method 例
    static void showStandardLibraryExamples() {
        // Collections.emptyList(): 空のリスト(変更不可)を返す Factory Method
        List<String> emptyList = Collections.emptyList();
        System.out.println("emptyList: " + emptyList + " (size=" + emptyList.size() + ")");

        // Collections.singletonList(): 1要素のリスト(変更不可)を返す Factory Method
        List<String> singleList = Collections.singletonList("Java");
        System.out.println("singleList: " + singleList + " (size=" + singleList.size() + ")");

        // Arrays.asList(): 配列から固定サイズのリストを返す Factory Method
        List<String> arrayList = Arrays.asList("Java 8", "Java 17", "Java 21");
        System.out.println("arrayList: " + arrayList + " (size=" + arrayList.size() + ")");
    }

    public static void main(String[] args) {
        System.out.println("=== Factory Method パターン ===");
        System.out.println();

        // 各ファクトリーでレポートを生成
        // 呼び出し側は ReportFactory 型として扱うだけでよい
        ReportFactory pdfFactory = new PdfReportFactory();
        ReportFactory htmlFactory = new HtmlReportFactory();
        ReportFactory csvFactory = new CsvReportFactory();

        String title = "月次売上レポート";
        String content = "2024年1月の売上合計は100万円でした。";

        System.out.println("[PDF ファクトリー]");
        System.out.println(pdfFactory.process(title, content));

        System.out.println();
        System.out.println("[HTML ファクトリー]");
        System.out.println(htmlFactory.process(title, content));

        System.out.println();
        System.out.println("[CSV ファクトリー]");
        System.out.println(csvFactory.process(title, content));

        System.out.println();
        System.out.println("=== Java 標準ライブラリの Factory Method 例 ===");
        showStandardLibraryExamples();
    }
}

Java 8 では抽象クラスと継承で Factory Method を実装します。createReport() が Factory Method で、サブクラスが具体的なクラスを生成します。呼び出し側は ReportFactory 型だけを知れば足ります。

よくあるミス・注意点

⚠️ Factory を使わず new で直接生成して依存が密結合になる

呼び出し側に new PdfReport() のような具体クラス名を直接書くと、 後でレポートの種類を増やしたり変更したりするときに呼び出し箇所を全て修正しなければなりません。 Factory を経由することで、呼び出し側は抽象型(インターフェース)だけに依存できます。

⚠️ Simple Factory と混同して if-else でクラスを分岐させる

Factory Method パターンの利点は「新しい種類を追加するときに既存クラスを修正しなくてよい」点です。 if-else での分岐(Simple Factory)は新種類追加のたびに既存コードの修正が必要になります。 これは開放閉鎖原則(拡張に対して開き、修正に対して閉じる)に反します。

⚠️ Collections.emptyList() の戻り値に要素を追加しようとする

Collections.emptyList() が返すリストは変更不可(immutable)です。 要素を追加しようとすると実行時に UnsupportedOperationException がスローされます。 変更可能なリストが必要なときは new ArrayList<>() を使いましょう。

テストする観点

  • PdfReportFactoryPdfReport のインスタンスを生成すること(正しい型のオブジェクトが返ること)
  • HtmlReportFactoryCsvReportFactory も同様に正しい型を返すこと
  • 各ファクトリーの process() が title と content を含む文字列を返すこと
  • PDF・HTML・CSV それぞれの出力形式(フォーマット)が正しいこと(境界値: title や content が空文字の場合)
  • title に null を渡したとき NullPointerException がスローされること(境界値)
  • 呼び出し側が ReportFactory 型として扱ったとき、ポリモーフィズムが正しく機能すること

GitHub でソースコードを見る →