抽象ファクトリーパターンとは ファクトリーメソッドパターンは、シンプルなファクトリーデザインが抱える責任過多の問題に対し、ファクトリーの階層構造を導入することで解決策をもたらしました。しかし、各ファクトリーが単一の製品カテゴリーしか扱わないため、システム内に多くのファクトリークラスが発生し、全体的なオーバーヘッドが増大する可能性があります。このような状況で検討されるのが、関連する複数の製品群を「製品ファミリー」としてまとめ、一つのファクトリーで一括して生成するというアプローチです。これが抽象ファクトリーパターンの基本的な考え方となります。
UIライブラリのテーマ切り替え問題 あるソフトウェア会社が、Javaデスクトップアプリケーション向けのUIスキンライブラリを開発するシナリオを考えてみましょう。このライブラリは、著作権保護のためソースコードは非公開とし、jarファイルとして提供されます。ユーザーはメニューからスキンを選択でき、選択したスキンに応じてボタン、テキストフィールド、コンボボックスといったUI要素の視覚効果が変わる必要があります。
このライブラリには、高い柔軟性と拡張性が求められます。ユーザーは自由にスキンを選択でき、開発者は既存のコードを変更することなく新しいスキンを追加できる必要があります。
当初、開発チームはファクトリーメソッドパターンを採用し、システムの柔軟性と拡張性を確保しようとしました。ボタン、テキストフィールド、コンボボックスなどのUI要素を生成するために多数の具象ファクトリーを提供し、クライアントは抽象ファクトリーに対してプログラミングする設計です。
この設計には、以下のような問題点が浮上しました。
- クラスの爆発的な増加: 新しいスキンを追加する際、既存コードの変更は不要ですが、追加される個々のコンポーネント(例:ボタン、テキストフィールド)ごとに具象ファクトリーを追加する必要があるため、クラス数が大幅に増加します。これにより、システムの肥大化とメンテナンスコストの増大、実行時オーバーヘッドの増加を招きます。
- 製品の一貫性維持の複雑さ: 同じスタイルのUIコンポーネントは通常、セットで表示されるべきです。しかし、各コンポーネントに対して個別に具象ファクトリーを選択する必要があるため、ユーザーが設定を誤ると、UI表示が不整合になる可能性があります。これを防ぐための制約を追加しても、クライアントコードや設定が複雑になってしまいます。
システム内のクラス数を減らし、かつクライアントが常に一貫したスタイルのUIコンポーネントを使用できるようにするにはどうすればよいでしょうか?この2つの問題は、ファクトリーメソッドパターンでは解決が困難です。そこで、抽象ファクトリーパターンが有効な解決策を提供します。
製品階層と製品ファミリー 抽象ファクトリーパターンを理解するために、二つの重要な概念を導入します。
- 製品階層 (Product Hierarchy): 製品の継承構造を指します。例えば、「抽象的なテレビ」があり、そのサブクラスとして「A社製テレビ」「B社製テレビ」「C社製テレビ」があるとします。この「抽象的なテレビ」と各ブランドのテレビが製品階層を構成し、抽象テレビが親クラス、具体的なブランドのテレビが子クラスとなります。
- 製品ファミリー (Product Family): 同じファクトリーによって生成される、異なる製品階層に属する製品群を指します。例えば、「A社製電化製品ファクトリー」が「A社製テレビ」と「A社製冷蔵庫」を生産するとします。「A社製テレビ」はテレビの製品階層に属し、「A社製冷蔵庫」は冷蔵庫の製品階層に属します。この「A社製テレビ」と「A社製冷蔵庫」が製品ファミリーを構成します。
抽象ファクトリーパターンでは、システムが提供するファクトリーが、単一のオブジェクトではなく、異なる製品階層に属する複数の異なるタイプの具象製品を生成する場合に利用されます。抽象ファクトリーパターンは、すべてのファクトリーパターンの中で最も抽象的で汎用性の高い形式の一つです。ファクトリーメソッドパターンが単一の製品階層に特化しているのに対し、抽象ファクトリーパターンは複数の製品階層に対応します。一つのファクトリー階層が、異なる製品階層に属する製品ファミリー内のすべてのオブジェクトの生成を担当できる場合、抽象ファクトリーパターンはファクトリーメソッドパターンよりもシンプルかつ効率的です。
抽象ファクトリーパターンの構造では、各具象ファクトリーが、一つの製品ファミリーに属するすべての製品(例えば、特定の色で統一された正方形、円形、楕円形)を生成します。これらの製品は、それぞれ異なる製品階層に属します。もしこれをファクトリーメソッドパターンで実現しようとすると、多数の具象ファクトリーが必要になるでしょうが、抽象ファクトリーパターンでは、製品ファミリーの数と同じ数の具象ファクトリーで済み、システム内のクラス数を大幅に削減できます。
抽象ファクトリーパターンの概要 抽象ファクトリーパターンは、関連するオブジェクト群を生成するための解決策を提供します。ファクトリーメソッドパターンと比較して、抽象ファクトリーパターンにおける具象ファクトリーは、単一の製品だけでなく、一つの製品ファミリー全体を生成する責任を持ちます。
抽象ファクトリーパターン (Abstract Factory Pattern): 関連または相互に依存するオブジェクト群を生成するためのインターフェースを提供しますが、それらの具体的なクラスを指定する必要はありません。このパターンは「Kitパターン」とも呼ばれ、生成に関するデザインパターンの一つです。
抽象ファクトリーパターンでは、各具象ファクトリーが複数のファクトリーメソッドを提供し、様々なタイプの製品を生成します。これらの製品が製品ファミリーを構成します。
抽象ファクトリーパターンの主要な構成要素は以下の通りです。
- AbstractFactory(抽象ファクトリー): 製品ファミリーを構成する各種製品を生成するためのメソッド群を宣言するインターフェースまたは抽象クラス。
- ConcreteFactory(具象ファクトリー): 抽象ファクトリーで宣言された製品生成メソッド群を実装し、特定の製品ファミリーを構成する具象製品群を生成します。各製品は特定の製品階層に属します。
- AbstractProduct(抽象製品): 各製品のインターフェース(または抽象クラス)を宣言します。製品が持つべき共通のビジネスロジックや操作を定義します。
- ConcreteProduct(具象製品): 具象ファクトリーによって生成される具体的な製品オブジェクトです。抽象製品インターフェースで宣言されたメソッドを実装します。
抽象ファクトリーは複数のファクトリーメソッドを宣言し、異なるタイプの製品を生成します。これはインターフェース、抽象クラス、または具象クラスのいずれでも構いません。典型的なコードは以下のようになります。
public interface AbstractWidgetFactory {
AbstractButton createButton(); // 製品Aの生成メソッド
AbstractTextField createTextField(); // 製品Bの生成メソッド
// ... その他の製品生成メソッド
}
具象ファクトリーは抽象ファクトリーを実装します。各具象ファクトリーメソッドは特定の製品オブジェクトを返します。同じ具象ファクトリーによって作成された製品オブジェクト群が製品ファミリーを構成します。各具象ファクトリークラスの典型的なコードは次のようになります。
public class ModernWidgetFactory implements AbstractWidgetFactory {
@Override
public AbstractButton createButton() {
return new ModernButton();
}
@Override
public AbstractTextField createTextField() {
return new ModernTextField();
}
// ... その他の製品生成メソッド
}
UIスキンライブラリへの適用 前述のソフトウェア開発会社は、UIスキンライブラリの設計を抽象ファクトリーパターンで再構築することにしました。
この構造では、ThemeFactory インターフェースが抽象ファクトリーの役割を担い、その実装クラスである SakuraThemeFactory や OceanThemeFactory が具象ファクトリーとして機能します。Button、InputBox、Dropdown インターフェースは抽象製品であり、SakuraButton、SakuraInputBox、SakuraDropdown および OceanButton、OceanInputBox、OceanDropdown が具象製品となります。
以下に、簡略化されたコード例を示します。実際のUIコンポーネントの初期化はより複雑ですが、ここではパターンの中核部分に焦点を当てます。
// 本例ではコードを大幅に簡略化しています。実際の使用では、UIコンポーネントの初期化コードはより複雑であり、
// JDKの既存クラスを使用する必要がある場合があります。ここでは中核となるコードとデモ出力のみを提供します。
// ボタンインターフェース:抽象製品
interface Button {
void render(); // 表示メソッド
}
// 桜テーマのボタン:具象製品
class SakuraButton implements Button {
@Override
public void render() {
System.out.println("桜色のボタンを表示します。");
}
}
// 海テーマのボタン:具象製品
class OceanButton implements Button {
@Override
public void render() {
System.out.println("青い波模様のボタンを表示します。");
}
}
// 入力ボックスインターフェース:抽象製品
interface InputBox {
void render(); // 表示メソッド
}
// 桜テーマの入力ボックス:具象製品
class SakuraInputBox implements InputBox {
@Override
public void render() {
System.out.println("桜色の枠線を持つ入力ボックスを表示します。");
}
}
// 海テーマの入力ボックス:具象製品
class OceanInputBox implements InputBox {
@Override
public void render() {
System.out.println("海色の枠線を持つ入力ボックスを表示します。");
}
}
// ドロップダウンインターフェース:抽象製品
interface Dropdown {
void render(); // 表示メソッド
}
// 桜テーマのドロップダウン:具象製品
class SakuraDropdown implements Dropdown {
@Override
public void render() {
System.out.println("桜色のドロップダウンメニューを表示します。");
}
}
// 海テーマのドロップダウン:具象製品
class OceanDropdown implements Dropdown {
@Override
public void render() {
System.out.println("海色のドロップダウンメニューを表示します。");
}
}
// UIテーマファクトリーインターフェース:抽象ファクトリー
interface ThemeFactory {
Button createButton();
InputBox createInputBox();
Dropdown createDropdown();
}
// 桜テーマファクトリー:具象ファクトリー
class SakuraThemeFactory implements ThemeFactory {
@Override
public Button createButton() {
return new SakuraButton();
}
@Override
public InputBox createInputBox() {
return new SakuraInputBox();
}
@Override
public Dropdown createDropdown() {
return new SakuraDropdown();
}
}
// 海テーマファクトリー:具象ファクトリー
class OceanThemeFactory implements ThemeFactory {
@Override
public Button createButton() {
return new OceanButton();
}
@Override
public InputBox createInputBox() {
return new OceanInputBox();
}
@Override
public Dropdown createDropdown() {
return new OceanDropdown();
}
}
システムの柔軟性と拡張性を高めるため、ファクトリークラスのインスタンスを動的にロードするユーティリティと設定ファイルを導入します。
ConfigLoader クラスのコードは以下の通りです。
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
public class ConfigLoader {
// XML設定ファイルから具象クラス名を抽出し、インスタンスオブジェクトを返すメソッド
public static Object loadFactory() {
try {
// ドキュメントオブジェクトを生成
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("theme_config.xml"));
// クラス名を含むテキストノードを取得
NodeList nodeList = doc.getElementsByTagName("factoryClass");
Node classNode = nodeList.item(0).getFirstChild();
String className = classNode.getNodeValue();
// クラス名を通じてインスタンスオブジェクトを生成して返す
Class<?> clazz = Class.forName(className);
Object obj = clazz.getDeclaredConstructor().newInstance(); // Java 9+対応
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
設定ファイル theme_config.xml には、使用する具象ファクトリークラス名が記述されます。
<?xml version="1.0"?>
<config>
<factoryClass>SakuraThemeFactory</factoryClass>
</config>
以下のクライアントテストコードを実行します。
class ThemeApplication {
public static void main(String[] args) {
ThemeFactory currentFactory;
Button uiButton;
InputBox uiInputBox;
Dropdown uiDropdown;
// 設定ファイルからファクトリーをロード
currentFactory = (ThemeFactory) ConfigLoader.loadFactory();
// ファクトリーを使用してUIコンポーネントを生成
uiButton = currentFactory.createButton();
uiInputBox = currentFactory.createInputBox();
uiDropdown = currentFactory.createDropdown();
// コンポーネントを表示
uiButton.render();
uiInputBox.render();
uiDropdown.render();
}
}
プログラムをコンパイルして実行すると、以下の結果が出力されます。
桜色のボタンを表示します。 桜色の枠線を持つ入力ボックスを表示します。 桜色のドロップダウンメニューを表示します。
もしテーマを変更したい場合は、theme_config.xml の factoryClass の値を OceanThemeFactory に変更するだけで済みます。実際のアプリケーションでは、メニューや設定画面からこの設定ファイルを変更できるようにすることで、ユーザーは直接ファイルを編集することなくテーマを切り替えることができます。新しいテーマを追加する際は、新しい具象コンポーネント群を実装し、それに対応する新しい具象ファクトリーを追加するだけで、既存のコードを変更する必要はありません。これは「開放/閉鎖原則」に準拠しています。
拡張性と「開放/閉鎖原則」の傾き
前述のソフトウェア会社は、抽象ファクトリーパターンを用いてUIスキンライブラリを設計し、新しいテーマの追加は容易になりました。しかし、設計時にラジオボタン(RadioButton)のような新しいUIコンポーネントのスタイリングを考慮し忘れたという深刻な問題に直面しました。どのスキンを選択しても、ラジオボタンだけが既存のテーマと一致せず、違和感を与えてしまいます。
開発チームはシステムにラジオボタンを追加することを決定しましたが、既存の「開放/閉鎖原則」を維持したままでは新しいコンポーネントを追加できないことに気づきました。その理由は、抽象ファクトリーインターフェースである ThemeFactory に、ラジオボタンを作成するためのメソッドが用意されていないためです。もしラジオボタンを追加する必要がある場合、まず ThemeFactory インターフェースにラジオボタンを生成するメソッドの宣言を追加し、次に既存のすべての具象ファクトリークラス(SakuraThemeFactory や OceanThemeFactory など)を修正して、対応するメソッドを追加する必要があります。さらに、クライアントコードも変更しないと、新しいラジオボタンは既存のシステムで利用できません。
これは抽象ファクトリーパターンが持つ最大の欠点であり、この問題自体は抽象ファクトリーパターンでは解決できません。抽象ファクトリーパターンでは、新しい製品ファミリー(新しいテーマ)の追加は非常に容易ですが、新しい製品階層(新しいコンポーネントタイプ、例:ラジオボタン)の追加は非常に困難です。抽象ファクトリーパターンのこの特性は、「開放/閉鎖原則」の傾き (tilt) と呼ばれます。
「開放/閉鎖原則」は、システムが拡張に対しては開放的であるべきだが、修正に対しては閉鎖的であるべきだと要求します。複数の製品ファミリーと複数の製品階層に関わるシステムにおいて、機能強化は大きく分けて以下の二つが考えられます。
- 製品ファミリーの追加: 新しい製品ファミリー(例:新しいテーマ)を追加する場合、抽象ファクトリーパターンは「開放/閉鎖原則」を良好にサポートします。新しい具象製品とそれに対応する新しい具象ファクトリーを追加するだけでよく、既存のコードに変更は必要ありません。
- 新しい製品階層の追加: 新しい製品階層(例:ラジオボタン)を追加する場合、抽象ファクトリークラスを含むすべてのファクトリー関連の役割が変更を必要とします。すべてのファクトリークラスに新しい製品を生成するメソッドを追加する必要があり、これは「開放/閉鎖原則」に違反します。
抽象ファクトリーパターンは、「開放/閉鎖原則」に対してこのような傾きを持つため、新しい製品ファミリーの追加を容易にする一方で、新しい製品階層の追加に対しては同様の柔軟性を提供しません。したがって、設計者は初期段階で包括的に考慮し、後から新しい製品階層を追加したり、既存の製品階層を削除したりする必要が生じないようにすることが求められます。そうしないと、システムに大幅な変更が必要となり、その後のメンテナンス作業が複雑化する原因となります。
抽象ファクトリーパターンまとめ 抽象ファクトリーパターンは、ファクトリーメソッドパターンをさらに発展させたものです。より強力なファクトリー機能を提供し、優れた拡張性を持つため、ソフトウェア開発、特にフレームワークやAPIライブラリの設計で広く利用されています。例えば、Java言語のAWT(Abstract Window Toolkit)では抽象ファクトリーパターンが使用され、異なるオペレーティングシステム上でアプリケーションがそのOS固有のルックアンドフィールを表現できるようにしています。これは、ソフトウェア開発で最も一般的に使用されるデザインパターンの一つです。
- 主な利点 抽象ファクトリーパターンの主な利点は以下の通りです。
- 具象クラス生成からの隔離: 抽象ファクトリーパターンは、具象クラスの生成をクライアントから隔離します。これにより、クライアントは具体的に何が作成されるかを知る必要がありません。この隔離により、具象ファクトリーを置き換えることが比較的容易になり、特定の具象ファクトリーのインスタンスを変更するだけで、システム全体の動作をある程度変更できます。
- 製品ファミリーの一貫性保証: 一つの製品ファミリー内の複数のオブジェクトが連携して動作するように設計されている場合、クライアントが常に同じ製品ファミリー内のオブジェクトのみを使用することを保証できます。
- 製品ファミリー追加の容易さ: 新しい製品ファミリーを追加することは非常に容易であり、既存のシステムを変更する必要がないため、「開放/閉鎖原則」に準拠します。
- 主な欠点 抽象ファクトリーパターンの主な欠点は以下の通りです。
- 新しい製品階層追加の困難さ: 新しい製品階層を追加する際、既存のシステムに大規模な変更が必要となり、抽象層のコードまで修正する必要がある場合があります。これは大きな不便をもたらし、「開放/閉鎖原則」に違反します。
- 適用シナリオ 以下の状況で抽象ファクトリーパターンの使用を検討できます。
- オブジェクト生成の詳細からの分離: システムが、製品クラスのインスタンスがどのように作成され、構成され、表現されるかの詳細に依存すべきでない場合。これはすべてのファクトリーパターンに共通の重要な点であり、ユーザーはオブジェクトの作成プロセスを意識する必要がなく、オブジェクトの作成と使用が分離されます。
- 複数の製品ファミリーと単一ファミリーの使用: システム内に複数の製品ファミリーが存在し、毎回そのうちの一つの製品ファミリーのみを使用する場合。設定ファイルなどを用いてユーザーが動的に製品ファミリーを変更でき、新しい製品ファミリーの追加も容易です。
- 同一製品ファミリー内での連携: 同じ製品ファミリーに属する製品が一緒に使用されるという制約がシステム設計に反映されるべき場合。同一製品ファミリー内の製品は直接的な関係がなくても、特定の共通の制約を持つことがあります。例えば、同じオペレーティングシステム下のボタンとテキストフィールドは直接関係ありませんが、どちらも特定のOSに属するという共通の制約を持ちます。
- 安定した製品階層: 製品階層が安定しており、設計完了後に新しい製品階層を追加したり、既存の製品階層を削除したりする可能性が低い場合。