広告

Java : Abstract Factory パターン (図解/デザインパターン)

Abstract Factory パターンとは、GoF によって定義されたデザインパターンの1つです。
関連する複数のオブジェクトの生成処理を、動的にまとめて切り替えるための設計です。

本記事では、Abstract Factory パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Abstract Factory パターン(アブストラクト・ファクトリ・パターン)[1]とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 関連するインスタンス群を生成するための API を集約することによって、利用側がインスタンス群をまとめて変えられるようにし、さらに組み合わせ方を間違えないようにする[1]

Abstract Factory パターンとは、

  • 関連する複数のオブジェクトがあり、
  • そのオブジェクトの生成処理を、動的に まとめて 切り替える

という設計です。

また、

  • 生成するオブジェクトの組み合わせを間違えないようにする

という効果もあります。

Abstract Factory は、日本語的に発音すると「アブストラクト・ファクトリ」となります。
意味は「抽象的な工場」ですね。


【Abstract Factory パターンのイメージ】

イメージ図1

ファクトリA は「製品A」を作るための部品 …

  • 部品A-1
  • 部品A-2
  • 部品A-3

を生成します。
一方、ファクトリB は「製品B」を作るための部品を生成します。

ファクトリを切り替えれば、生成する部品を

  • 部品A → 部品B

まとめて 切り替えられるのが特徴です。


イメージ図2

決まったファクトリから部品を生成していれば、

 「製品B」を作っているはずなのに「部品A」が混入してしまった …

なんてこともおきません。
Abstract Factory パターンのメリットの1つですね。


イメージ図3

また、ファクトリA、ファクトリB、ファクトリC ... と追加していことで、いくらでもファクトリを拡張できるのもメリットです。
しかも、ファクトリを使う側のクラスは修正が必要ありません。

いわゆる 開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

ですね。

さて、Abstract Factory パターンでは、関連する 複数 のオブジェクトを生成する … という説明をしてきました。
ただし、複数 にこだわる必要はありません。

1つの部品だけを作る Abstract Factory はもちろん有用です。
開放閉鎖の原則 によるメリットは得られるからです。

1つの部品だけを作るファクトリは、厳密には Abstract Factory パターンではないのかもしれません。
しかし、それは些細な問題でしょう。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Abstract Factory パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。

まずは、クラス図の右側の AbstractProduct インタフェースとその実装クラスです。

public interface AbstractProduct1 {
}

public class ProductA1 implements AbstractProduct1 {
    @Override
    public String toString() {
        return "Product A-1";
    }
}

public class ProductB1 implements AbstractProduct1 {
    @Override
    public String toString() {
        return "Product B-1";
    }
}
public interface AbstractProduct2 {
}

public class ProductA2 implements AbstractProduct2 {
    @Override
    public String toString() {
        return "Product A-2";
    }
}

public class ProductB2 implements AbstractProduct2 {
    @Override
    public String toString() {
        return "Product B-2";
    }
}

次に、クラス図の左側の AbstractFactory インタフェースとその実装クラスです。

public interface AbstractFactory {
    AbstractProduct1 createProduct1();

    AbstractProduct2 createProduct2();
}

public class ConcreteFactoryA implements AbstractFactory {
    @Override
    public AbstractProduct1 createProduct1() {
        return new ProductA1();
    }

    @Override
    public AbstractProduct2 createProduct2() {
        return new ProductA2();
    }
}

public class ConcreteFactoryB implements AbstractFactory {
    @Override
    public AbstractProduct1 createProduct1() {
        return new ProductB1();
    }

    @Override
    public AbstractProduct2 createProduct2() {
        return new ProductB2();
    }
}

それでは、これらのクラスを使ってみましょう。

final var factory = new ConcreteFactoryA();

final var product1 = factory.createProduct1();
final var product2 = factory.createProduct2();

System.out.println(product1); // Product A-1
System.out.println(product2); // Product A-2
final var factory = new ConcreteFactoryB();

final var product1 = factory.createProduct1();
final var product2 = factory.createProduct2();

System.out.println(product1); // Product B-1
System.out.println(product2); // Product B-2

結果も問題なしですね。


具体的な例

Abstract Factory パターンを使わないケース

もう少し具体的な例も見てみましょう。
次のような GUI を考えてみます。

UI図1

とはいえ、GUI をコード例にするのは複雑になるので、ここでは単純なテキスト出力のみで考えます。
(あくまで上記のダイアログはイメージということで…)

ダイアログは、UI の部品として

  • ラベル  : 「処理を実行しますか?」
  • ボタン1 : 「OK」
  • ボタン2 : 「CANCEL」

を持ちます。


クラス図2

さて、このダイアログですが、Windows と Mac の両方で使いたいとしましょう。
コードもなるべく使いまわせるようにしたいです。

ただ、ラベルとボタンは、どうしても Windows 用と Mac 用で分ける必要がでてきました。
上のクラス図でいうと、色のついたクラスが Windows 用と Mac 用のクラスです。

まずは、Abstract Factory パターンを 使わない で作ってみましょう。

public interface Label {
    String getText();
}

public interface Button {
    String getText();
}

Label と Button インタフェースは getText メソッドで文字列を返すだけです。

本来なら、Button インタフェースにはボタンを押したときの action メソッドがあったほうがよいでしょう。
ただ、今回はコード例を簡単にするために省きます。

public class Dialog {
    private final Label label;
    private final Button button1;
    private final Button button2;

    public Dialog(Label label, Button button1, Button button2) {
        this.label = label;
        this.button1 = button1;
        this.button2 = button2;
    }

    public void show() {
        System.out.println("---- ダイアログ ----");
        System.out.println("ラベル  : " + label.getText());
        System.out.println("ボタン1 : " + button1.getText());
        System.out.println("ボタン2 : " + button2.getText());
    }
}

Dialog クラスは、

  • ラベル (Label)
  • ボタン1 (Button)
  • ボタン2 (Button)

を持ちます。
そして、show メソッドでラベルとボタンのテキストを表示します。

次は、Label インタフェースの実装クラスです。

public class WinLabel implements Label {
    private final String text;

    public WinLabel(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Win)";
    }
}

public class MacLabel implements Label {
    private final String text;

    public MacLabel(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Mac)";
    }
}

Windows 用と Mac 用にそれぞれ実装します。

最後に、Button インタフェースの実装クラスです。

public class WinButton implements Button {
    private final String text;

    public WinButton(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Win)";
    }
}

public class MacButton implements Button {
    private final String text;

    public MacButton(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Mac)";
    }
}

こちらも Windows 用と Mac 用にそれぞれ実装します。

それでは、Windows 用の UI部品 を使ってダイアログを表示してみましょう。

final var label = new WinLabel("処理を実行しますか?");
final var button1 = new WinButton("OK");
final var button2 = new MacButton("CANCEL");

final var dialog = new Dialog(label, button1, button2);
dialog.show();

// 結果
// ↓
//---- ダイアログ ----
//ラベル  : 処理を実行しますか? (Win)
//ボタン1 : OK (Win)
//ボタン2 : CANCEL (Mac)

さて、実行した結果は問題なし … ではありませんね。
Windows 用のはずが、CANCEL ボタンだけ Mac 用になってしまいました。

final var button2 = new MacButton("CANCEL"); 
                        ^^^ <--- ★ 間違い!

このように、UI部品を直接 new すると、うっかり間違えてしまうことがあります。

今回は WinLabel と MacLabel というクラス名です。
どちらが Windows 用なのか Mac 用なのかは比較的わかりやすいと思います。

しかし、実際の開発現場では、このように分かりやすい名前とは限りません。
その場合、

 これはどっちの部品なんだろう? 本当にこっちで正しいのかな …?

ということを、いちいち確認しながらコーディングすることになります。
それは面倒ですし工数もかかります。

さらに、Mac 用のダイアログを表示したい場合は、先ほどのコードをコピペしてから

  • WinLabel -> MacLabel
  • WinButton -> MacButton

と手で書き換える必要があります。

final var label = new MacLabel("処理を実行しますか?");
final var button1 = new MacButton("OK");
final var button2 = new MacButton("CANCEL");

final var dialog = new Dialog(label, button1, button2);
dialog.show();

// 結果
// ↓
//---- ダイアログ ----
//ラベル  : 処理を実行しますか? (Mac)
//ボタン1 : OK (Mac)
//ボタン2 : CANCEL (Mac)

これは、コードの再利用性が悪いといえるでしょう。

Windows 用と Mac 用のほかに、Linux 用や Android 用などが増えるかもしれません。
そうなると、さらにややこしくなり、単純なコーディングミスも増えてしまいます。


Abstract Factory パターンを使うケース

それでは、Abstract Factory パターンを使って 先ほどの構成 を改善してみましょう。

クラス図3

Windows 用と Mac 用の UI部品(Label, Button) は直接 new しません。
UiFactory インタフェースを使って生成します。

クラス図の右側は、Abstract Factory パターンを使わないケース と同じです。
先ほどと同じですが、一応コードものせておきます。

public interface Label {
    String getText();
}

public interface Button {
    String getText();
}
public class Dialog {
    private final Label label;
    private final Button button1;
    private final Button button2;

    public Dialog(Label label, Button button1, Button button2) {
        this.label = label;
        this.button1 = button1;
        this.button2 = button2;
    }

    public void show() {
        System.out.println("---- ダイアログ ----");
        System.out.println("ラベル  : " + label.getText());
        System.out.println("ボタン1 : " + button1.getText());
        System.out.println("ボタン2 : " + button2.getText());
    }
}
public class WinLabel implements Label {
    private final String text;

    public WinLabel(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Win)";
    }
}

public class MacLabel implements Label {
    private final String text;

    public MacLabel(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Mac)";
    }
}
public class WinButton implements Button {
    private final String text;

    public WinButton(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Win)";
    }
}

public class MacButton implements Button {
    private final String text;

    public MacButton(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text + " (Mac)";
    }
}

次は、クラス図の左側のファクトリーです。

public interface UiFactory {
    Label createLabel(String text);

    Button createButton(String text);
}

UiFactory インタフェースでは、Label と Button を生成するメソッドを定義します。
あとは、Windows 用と Mac 用の実装クラスを作るだけです。

public class WinUiFactory implements UiFactory {
    @Override
    public Label createLabel(String text) {
        return new WinLabel(text);
    }

    @Override
    public Button createButton(String text) {
        return new WinButton(text);
    }
}

public class MacUiFactory implements UiFactory {
    @Override
    public Label createLabel(String text) {
        return new MacLabel(text);
    }

    @Override
    public Button createButton(String text) {
        return new MacButton(text);
    }
}

WinUiFactory クラスでは、Windows 用の UI部品である

  • WinLabel クラス
  • WinButton クラス

のインスタンスを生成します。
MacUiFactory クラスでは、Mac 用の UI部品を生成します。

やっていること自体は単純だと思います。

それでは、これらのクラスを使ってみましょう。
今回は、ファクトリを使ってダイアログを作成・表示する showDialog メソッドを用意します。

public void showDialog(UiFactory factory) {
    final var label = factory.createLabel("処理を実行しますか?");
    final var button1 = factory.createButton("OK");
    final var button2 = factory.createButton("CANCEL");

    final var dialog = new Dialog(label, button1, button2);
    dialog.show();
}
// -------------------
// Windows用
showDialog(new WinUiFactory());

// 結果
// ↓
//---- ダイアログ ----
//ラベル  : 処理を実行しますか? (Win)
//ボタン1 : OK (Win)
//ボタン2 : CANCEL (Win)

// -------------------
// Mac用
showDialog(new MacUiFactory());

// 結果
// ↓
//---- ダイアログ ----
//ラベル  : 処理を実行しますか? (Mac)
//ボタン1 : OK (Mac)
//ボタン2 : CANCEL (Mac)

無事に、ファクトリ経由で UI部品を作成してダイアログを表示することができました。

  • ファクトリを変えることで、UI部品を まとめて 切り替える
  • Windows 用と Mac 用の UI部品が 混在しない

この2つが、Abstract Factory パターンを使うメリットになります。

まとめ

Abstract Factory パターンとは、

  • 関連する複数のオブジェクトがあり、
  • そのオブジェクトの生成処理を、動的に まとめて 切り替える

という設計です。

また、

  • 生成するオブジェクトの組み合わせを間違えないようにする

という効果もあります。

関連する 複数 のオブジェクト … とありますが、複数 にこだわる必要はありません。
1つだけのオブジェクトを作成する Abstract Factory パターンも全然ありです。

ぜひ有効に活用していきたいですね。


関連記事

ページの先頭へ