本記事ではGoFのデザインパターンのオブジェクトの生成に関するパターンの一つである「Abstract Factory」パターンを解説します。このパターンを一言で説明するならば、「関連したオブジェクトの集まりを、具象クラスを指定しなくても生成することが可能になるパターン」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。
「Abstract Factory」パターンとは
Abstract Factoryを直訳すると「抽象的な工場」となります。「Abstract Factory」パターンとは先述した通り、「関連したオブジェクトの集まりを、具象クラスを指定しなくても生成することが可能になるパターン」です。
例を挙げてみましょう。あなたはカレーライスを作るプログラム作成しています。「カレーライス」インスタンスを作って、そこに「固形ルー」インスタンス、「ライス」インスタンス、「とんかつ」インスタンスを追加するプログラムにする場合下記になるかと思います。
Curry curryRice = new Curry();
curryRice.addStapleFood(new Rice()); // 米
curryRice.addRoux(new SolidRoux()); // 固形ルー
curryRice.addTopping(new PorkCutlet()); // とんかつ
RiceはStapleFood(主食)を継承したクラス、SolidRouxはRoux(ルー)クラスを継承したクラス、PorkCutletはTopping(トッピング)クラスを継承したクラスとします。
上記の例の場合、カレーライスが出来上がります。特に問題ないようですが、間違えて「うどん」クラスをaddStapleFoodで追加したとします。UdonクラスはRiceクラス同様にStapleFoodクラスを継承しているためaddStapleFoodに入れることができます。
Curry curryRice = new Curry();
curryRice.addStapleFood(new Udon()); // うどん
curryRice.addRoux(new SolidRoux()); // 固形ルー
curryRice.addTopping(new PorkCutlet()); // とんかつ
本来はカレーライスを作りたいのに、カレーうどんになってしまっています。このように、プログラマーのミスで、本来想定しているものとは全く違うものができてしまう可能性があります。
これを回避するために、カレーうどん、カレーライス、インドカレー、それぞれの専用の工場を作ってしまえば良いのです。カレーライスの工場からは必ず、RiceとSolidRouxとPorkCutletを出荷するようにすることで、プログラマーの意図から外れたオブジェクトが生成されることを防ぎます。
Abstract Factoryパターンの書き方
では実際に、カレーの例を用いてコードを確認していきます。まずは「Abstract Factory」を使わない方法です。カツカレーととインドカレーをコードベースで作成します。
✖︎ Abstract Factoryパターンを使用しない例 ✖︎
public class Main {
public static void main(String[] args) {
// カツカレーを作る
Curry porkCutletCurry = new PorkCutletCurry();
porkCutletCurry.addStapleFood(new Rice()); // 米
porkCutletCurry.addRoux(new SolidRoux()); // 固形ルー
porkCutletCurry.addTopping(new PorkCutlet()); // とんかつ
// インドカレーを作る
Curry indianCurry = new IndianCurry();
indianCurry.addStapleFood(new Naan()); // ナン
indianCurry.addRoux(new ButterChicken()); // バターチキン
indianCurry.addTopping(new TandooriChicken()); // タンドリーチキン
}
}
// カレーの親クラス
public abstract class Curry {
abstract public void addStapleFood(StapleFood stapleFood);
abstract public void addRoux(Roux roux);
abstract public void addTopping(Topping topping);
}
// カレークラスを継承した「カツカレー」クラス
public class PorkCutletCurry extends Curry {
private StapleFood stapleFood;
private Roux roux;
private Topping topping;
public PorkCutletCurry() {
}
@Override
public void addStapleFood(StapleFood stapleFood) {
this.stapleFood = stapleFood;
}
@Override
public void addRoux(Roux roux) {
this.roux = roux;
}
@Override
public void addTopping(Topping topping) {
this.topping = topping;
}
}
// その他のクラスは省略
PorkCutletCurryクラスとIndianCurryクラスはCurryクラスを継承しています。mainの中で「カツカレー」と「インドカレー」のクラスをnewして、それぞれが持っているフィールドに具材のインスタンスを立てて入れています。
一見問題なさそうですが、カツカレーを作るときの具材をやインドのカレーを作るときの具材をプログラマー自身に入れさせてしまっています。これでは、カツカレーを複数回別のインスタンスとして登場させるときに、間違いが生じてstapleFoodフィールドにUdonクラス(うどん)のインスタンスを入れてしまい、カツカレーが欲しいのにカレーうどんになりかねません。
⭕️ Abstract Factoryパターンを使用する例 ⭕️
上記の問題は、プログラマー自身に具材のインスタンスを何度もnewさせて、プログラマー自身の手で具材の設定をしていることで、間違いが生じやすいようになっていました。そこで、カツカレーの工場とインドカレーの工場を用意してみましょう。
public abstract class Factory {
abstract public StapleFood getStapleFood();
abstract public Roux getRoux();
abstract public Topping getTopping();
}
上記は工場の抽象クラスです。工場ではgetStapleFood()で主食の出荷をし、getRoux()でルーの出荷をし、getToppingでトッピングの出荷を行います。抽象メソッドなので実際に何を出荷するかは継承したクラスで確定させます。
public class PorkCutletCurryFactory extends Factory {
@Override
public StapleFood getStapleFood() {
// 米を出荷
return new Rice();
}
@Override
public Roux getRoux() {
// 固形ルーを出荷
return new SolidRoux();
}
@Override
public Topping getTopping() {
// とんかつを出荷
return new PorkCutlet();
}
}
public class IndianCurryFactory extends Factory {
@Override
public StapleFood getStapleFood() {
// ナンを出荷
return new Naan();
}
@Override
public Roux getRoux() {
// バターチキンカレーのルーを出荷
return new ButterChicken();
}
@Override
public Topping getTopping() {
// タンドリーチキンを出荷
return new TandooriChicken();
}
}
PorkCutletCurryFactoryとIndianCurryFactoryはカツカレー工場とインドカレー工場です。工場の雛形(抽象クラス)であるFactoryを継承しています。
Factory内のメソッドを継承したクラスで実装しています。カツカレーでは「米、固形ルー、カツ」を出荷しているのに対し、インドカレーでは「ナン、バターチキンカレーのルー、タンドリーチキン」を出荷しています。factory内でnewして、そのfactoryを呼び出してgetすることで具材の間違いをなくすことができます。
Factoryの呼び出し元を見てみましょう。
public class Main {
public static void main(String[] args) {
// カツカレーを作る
Curry porkCutletCurry = new porkCutletCurry();
Factory porkCutletCurryFactory = createFactory(CurryType.PORK_CUTLET);
porkCutletCurry.addStapleFood(porkCutletCurryFactory.getStapleFood());
porkCutletCurry.addRoux(porkCutletCurryFactory.getRoux());
porkCutletCurry.addTopping(porkCutletCurryFactory.getTopping());
// インドカレーを作る
Curry indianCurry = new IndianCurry();
Factory indianCurryFactory = createFactory(CurryType.INDIAN);
indianCurry.addStapleFood(indianCurryFactory.getStapleFood());
indianCurry.addRoux(indianCurryFactory.getRoux());
indianCurry.addTopping(indianCurryFactory.getTopping());
}
// それぞれのカレー工場を作るメソッド
private static Factory createFactory(CurryType curryType) {
return switch (curryType) {
case PORK_CUTLET -> new PorkCutletCurryFactory();
case INDIAN -> new IndianCurryFactory();
default -> throw new IllegalArgumentException("指定されたカレーは作れません");
};
}
}
public enum CurryType {
PORK_CUTLET, // カツカレー
INDIAN, // インドカレー
}
createFactoryでは、引数に与えられたenumの値を見てどの工場を作るか場合分けを行い、Factoryクラスとして返却できるようにしています。mainクラスの中ではfactoryがどの型かを知ることなくカレー作りが進行しています。このようにすることで、工場を丸ごと変て最小限の変更でオブジェクトの生成を丸ごと変更することが可能です。
また、プログラマーがカレーを作るときに指定しなければならない情報は、カレーの種類だけに絞られるため、意図しないカレーが出来上がることも防ぐことができます。
まとめ
上記のカレーのクラス図は下記になります。
このようにAbstract Factoryは抽象的な工場を作ることで、プログラマーのミスを防ぎ、最小限の変更でオブジェクトの生成を行うことができます。
本記事で利用したソースコードは下記で公開しております。
https://github.com/tamotech1028/abstract-factory
コメント