ファクトリーメソッドパターンとは?
オブジェクト生成の責任をサブクラス(下位クラス)に委譲します。
ファクトリーメソッドの核心は、オブジェクト生成(インスタンス生成)について、誰が「委譲」をし、誰が「責任」(生成)を負うのかという点にあります。
問題点
オブジェクト生成時、new演算子を通じて生成を行ってみましょう。
私たちのドメインは寿司屋であり、ドメインコードは SushiRestaurant クラスに定義されます。
注文が入ると、私たちはそれに合った寿司を作り出します。
public class SushiRestaurant {
public Sushi order(String type) {
if (type.equals("サーモン")) {
SalmonSushi sushi = new SalmonSushi(); // 直接生成
sushi.make();
return sushi;
}
else if (type.equals("マグロ")) {
TunaSushi sushi = new TunaSushi(); // また直接生成
sushi.make();
return sushi;
}
}
}
もしここで、エビ、イカ、ヤリイカなど、寿司のメニューを拡張(ドメイン拡張)した場合、どうなるでしょうか。
public Sushi order(String type) {
if (type.equals("サーモン")) {
SalmonSushi sushi = new SalmonSushi(); // 直接生成
sushi.make();
return sushi;
}
else if (type.equals("マグロ")) {
TunaSushi sushi = new TunaSushi(); // また直接生成
sushi.make();
return sushi;
}
else if(type.equals("エビ")){
...
}
else if(type.equals("ヤリイカ")){
...
}
... // 数多くの else if 文
メニューが追加されるたびに、既存の order メソッドを直接修正しなければならないという問題が発生します。
これは、オブジェクト指向の中核となる原則である OCP(開放閉鎖の原則、Open-Closed Principle) に違反します。
Simple Factory pattern(シンプルファクトリーパターン)
「オブジェクト生成(寿司を作る)」責任を切り出す方法で、リファクタリングを行ってみます。
シェフを一人、雇ってみましょうか?
// シェフ(Simple Factory)
public class SushiChef {
public Sushi createSushi(String type) {
if (type.equals("サーモン")) {
return new SalmonSushi();
}
else if (type.equals("マグロ")) {
return new TunaSushi();
}
else if (type.equals("エビ")) {
return new ShrimpSushi();
}
}
}
それでは、これでお店は
// お店
public class SushiRestaurant {
private SushiChef chef = new SushiChef();
public Sushi order(String type) {
Sushi sushi = chef.createSushi(type); // シェフに委譲
sushi.make();
return sushi;
}
}
もう店長(お店)は、どんな寿司なのかを知る必要はありません。
ただシェフに注文(order)を通して伝えればよいのです。
もう少しアップグレードしてみましょう。
ファクトリーオブジェクトをシングルトンパターンで管理して、コストを削減する
現在は、注文 order() が入るたびに new SushiChef を通じて、新しいシェフを雇い続けています。
// お店
public class SushiRestaurant {
// 注文のたびにシェフを新しく呼ぶ(非効率的)
public Sushi order(String type) {
SushiChef chef = new SushiChef();
Sushi sushi = chef.createSushi(type);
...
}
}
これは、オブジェクトを生成するたびにメモリや速度など、オブジェクト生成のコストがかかるということです。
これを解決するために、シェフをたった一人だけ雇っておき、注文が入るたびにそのシェフを使い続ける(再利用する)のはどうでしょうか?
すでにシングルトンパターンで、オブジェクトの再生成防止について学んだはずです。
そうすると、次のようになります。
public class SushiChef {
// 1. あらかじめシェフを一人雇っておきます(static変数)
private static final SushiChef instance = new SushiChef();
// 2. 外部から勝手にシェフをまた雇えないように防ぎます(privateコンストラクタ)
private SushiChef() {}
// 3. あらかじめ雇っておいたシェフを連れてくるメソッド
public static SushiChef getInstance() {
return instance;
}
public Sushi createSushi(String type) {
if (type.equals("サーモン")) {
return new SalmonSushi();
}
else if (type.equals("マグロ")) {
return new TunaSushi();
}
}
}
次のように、あらかじめシェフを雇っておく方法を使用できます。
ただし、キャッシュされたオブジェクトには、状態値(メンバ変数)を持たせないこと(ステートレス)が望ましいです。
これで、お店のコードは次のようにシェフを再利用する構造に変更されます。
public class SushiRestaurant {
public Sushi order(String type) {
// その都度 new せず、すでに存在するシェフを呼び出します
SushiChef chef = SushiChef.getInstance();
Sushi sushi = chef.createSushi(type);
sushi.make();
return sushi;
}
}
これで数十、数百、数千回の注文が入っても、効率的に寿司を作ることができるようになりました。
シンプルファクトリーパターンのメリットと限界
シンプルファクトリーパターンを通じて、次のようなメリットが得られました。
- 単一責任の原則を守りました
- 以前は店長が注文も受け、寿司も作っていましたが、
- 今では店長は注文だけ、シェフは製造だけを担当するようになりました
- これにより、クライアントコードがすっきりしました
Sushi sushi = chef.createSushi(type); // ドメインコードが簡潔になりました
限界
依然として、以前発生していた if-else 分岐の問題はまだ解決されていません。
もし、新しいメニューをたくさん追加することになったらどうなるでしょうか。
public class SushiFactory {
public Sushi createSushi(String type) {
if (type.equals("サーモン")) {
return new SalmonSushi();
}
else if (type.equals("マグロ")) {
return new TunaSushi();
}
else if (type.equals("エビ")) {
return new ShrimpSushi();
}
else if (type.equals("玉子")){
...
}
else if (type.equals("ヤリイカ")){
...
}
else if (type.equals("サバ")){
...
}
// else if..
}
}
依然として、拡張に対して閉じているコードです(OCP違反)。
新しいメニューを追加するたびに、既存の SushiChef クラスを修正しなければなりません。
これを解決するために、ファクトリーメソッドパターンを使用することができます。
シングルトンパターン参考資料
ファクトリーメソッドパターン (Factory method pattern)
シェフ一人ではなく、各寿司の職人(専門家)を招いて、各自がそれぞれの寿司に責任を持つようにしましょう。
これは多態性(ポリモーフィズム)を利用して、if-elseの分岐パターンを解決することができます。
public abstract class SushiRestaurant {
abstract Sushi createSushi();
public Sushi order() {
Sushi sushi = createSushi();
sushi.make();
return sushi;
}
}
public class SalmonRestaurant extends SushiRestaurant {
@Override
Sushi createSushi() {
// 子クラスが「どのオブジェクトを作るか」を決定します
return new SalmonSushi();
}
}
public class TunaRestaurant extends SushiRestaurant {
@Override
Sushi createSushi() {
return new TunaSushi();
}
}
SushiRestaurant salmonJib = new SalmonRestaurant();
salmonJib.order();
SushiRestaurant tunaJib = new TunaRestaurant();
tunaJib.order();
次のようにファクトリーメソッドパターンを使用して、シンプルファクトリーパターンの限界(if-elseの地獄)から脱却できます。
もし新しい寿司(ヒラメ)を追加する場合、既存のコードを修正する必要なく、新しいサブクラスをもう一つ作成するだけで済みます。
// ヒラメ専門店(継承を通じて拡張)
public class FlatfishRestaurant extends SushiRestaurant { // 親クラスを継承
@Override
Sushi createSushi() {
return new FlatfishSushi();
}
}
店長(親クラス)は、具体的にどんな寿司が作られるのかを知る必要はありません。ただ createSushi() が呼び出されれば、子クラスで定義した通りに適切な寿司が出てくるという事実だけを知っていればよいのです。
自然とファクトリーメソッドパターンは、**OCP(開放閉鎖の原則)**を遵守することになります。
- 拡張に対しては開かれている:
FlatfishRestaurantを追加して拡張可能 - 変更に対しては閉じている:既存の
SushiRestaurantやorder()メソッドを修正する必要がない
ファクトリーメソッドパターンの限界
結合がなくなったと考えがちです。
しかし、実際に結合がなくなったわけではありません。
// お店(親)のコードはすっきりしましたが…
public abstract class SushiRestaurant {
public Sushi order() {
Sushi sushi = createSushi(); // 具体的なことは子クラスが作ります
sushi.make();
return sushi;
}
abstract Sushi createSushi();
}
店長のコードがすっきりしたのは確かです。
しかし、どこかで必ず誰かが、具体的なクラスを選択しなければなりません。
// 誰かは結局、具体的なクラス(SalmonRestaurant)を知らなければなりません!
SushiRestaurant restaurant = new SalmonRestaurant();
restaurant.order();
つまり、オブジェクト生成の結合が別の場所に移動しただけです。
相変わらずどこかで new SalmonRestaurant() というように、具体的なクラスを明示しなければなりません。
また、単に寿司のメニューを一つ増やすために(オブジェクト生成のために)、不必要にクラスファイルが増え続けるというデメリットも存在します。
ファクトリーのファクトリーで、ファクトリーメソッドの結合問題を解決しましょう
以前の問題は、ファクトリーを管理するファクトリー(Factory Map)で解決することができます。
まるでデリバリーアプリのように、どの食堂(Factory)を利用すべきかを代わりに探してくれる役割を作るのです。
// 食堂(Factory)を生成・管理するファクトリー
public class RestaurantMap {
private static final Map<String, SushiRestaurant> map = new HashMap<>();
static {
// あらかじめ各専門店(Factory)を登録しておきます
map.put("サーモン", new SalmonRestaurant());
map.put("マグロ", new TunaRestaurant());
// map.put("ヒラメ", new FlatfishRestaurant());
}
public static SushiRestaurant getRestaurant(String type) {
return map.get(type);
}
}
次のようにすれば、クライアントは具体的なクラス(new SalmonRestraunt)を知る必要がなくなります。
// クライアントは "サーモン" という文字列だけ知っていればよいです。(具体クラスを知る必要なし)
SushiRestaurant restaurant = RestaurantMap.getRestaurant("サーモン");
// お店が自動的に寿司を生成して提供
restaurant.order();
こうすれば、具体クラス(SalmonRestaurant)に対する依存性を、RestaurantMapという一箇所に集約することができます。
ただし、こうしてもOCPの解決にはなりません…。
依然として残る限界点
- クラスが増えすぎてしまいます(Class Explosion / クラス爆発)
寿司屋が繁盛して寿司のメニューを増やすたびに、Restaurantクラスが一つずつ増えていきます。
わずか短いコード(寿司作り)のために数十個ものクラスを作らなければならないため、プロジェクトの複雑度が増し、管理が難しくなります。
(オーバーエンジニアリングの発生)
- 結局、結合が移動しただけであり、解決されたわけではありません
// どこかで結局呼び出されます
map.put("サーモン", new SalmonRestaurant());
新しいメニューが追加されると、結局開発者が RestaurantMap クラスを開いて、static ブロックの中に直接 new を呼び出すコードを入れなければなりません。
つまり、ファクトリーをファクトリーでラップして、いくら抽象化しても、プログラムのどこかでは必ず new を通じてオブジェクトを生成しなければなりません。
この問題を解決する方法こそが、DI(Dependency Injection)コンテナです。(次の記事で詳しく解説します)
DIコンテナとは
-
DIコンテナとは、リフレクション(reflection)を通じて、開発者がいちいちファクトリークラスを作らなくても、オブジェクト生成と依存性の注入を自動化してくれる、ファクトリーパターンの拡張型です
-
DIコンテナは、多くのオブジェクトを格納できるため、コンテナと呼ばれます
抽象ファクトリーパターン (Abstract Factory Pattern)
お客さんが増えるにつれて、寿司だけでなく、刺身、汁物など、様々なメニューが追加されました。
注文が複雑になり、お互いに合わない料理が提供されるミスが発生しました。
// 別々に生成していたら…
Sushi sushi = new SalmonSushi();
Sashimi sashimi = new TunaSashimi(); // あれ?サーモンセットにマグロの刺身が?
関連する商品(サーモンの寿司、サーモンの汁物、サーモンの刺身)を一つのセットとしてまとめて管理する必要性が生じました。
このために、抽象ファクトリーパターンAbstract Factory Patternが始まります。
各セットメニュー(製品群)の生成を担当するファクトリー(Factory)を作成することにしました。
public interface CourseFactory {
Sushi createSushi();
Soup createSoup();
Sashimi createSashimi();
}
public class SalmonCourseFactory implements CourseFactory {
@Override
public Sushi createSushi() { return new SalmonSushi(); }
@Override
public Soup createSoup() { return new SalmonSoup(); }
@Override
public Sashimi createSashimi() { return new SalmonSashimi(); }
}
public class TunaCourseFactory implements CourseFactory {
@Override
public Sushi createSushi() { return new TunaSushi(); }
@Override
public Soup createSoup() { return new TunaSoup(); }
@Override
public Sashimi createSashimi() { return new TunaSashimi(); }
}
これで、店長は個別のメニューを気にする必要はなく、どのコースなのかだけを知っていればよいのです。
// 店長
public class SushiCourseRestaurant {
public void serveCourse(CourseFactory factory) {
// factoryが何であっても、この中で生成されるオブジェクトはすべて「同じ種類」であることが保証されます。
Sushi sushi = factory.createSushi();
Soup soup = factory.createSoup();
Sashimi sashimi = factory.createSashimi();
sushi.make();
soup.make();
sashimi.make();
}
}
抽象ファクトリーパターンのメリット
抽象ファクトリーパターンを使用すれば、関連するオブジェクト間の一貫性を強制することができます。
// 従来の方法(ミスが起こりうる)
// 開発者が誤って、サーモン寿司にマグロのスープを合わせてしまう可能性があります
Sushi sushi = new SalmonSushi();
Soup soup = new TunaSoup();
// 抽象ファクトリーパターン(ミスは起こりえません)
// 'SalmonCourseFactory'を選択した瞬間、すべてのメニューがサーモンに統一されます
CourseFactory factory = new SalmonCourseFactory();
restaurant.serveCourse(factory);
リファレンス