この記事は?
オライリー社から出版されている「HeadFirst デザインパターン」を読んだ内容を
理解の整理のためTypescriptで実装してみる内容です。(定期で載せれる様がんばる)
対象となるデザインパターン
参考書の「Factoryパターン」の章で紹介されているのは、以下の3つ。
- Simple Factory
- Gofのデザインパターンじゃないけど、よく見かける実装方法で、本書内では「Simple Factory」というデザインパターン名で紹介されています。
- 内容的にはシンプルなので、今回は省略します。
- Factory Method
- Factoryシリーズの一つ。こちらは別の記事でまとめてます。
- AbstractFactory
- 今回対象となるデザインパターン。概要はWiki様参照ください。
Abstract Factoryについて
参考書では FactoryMethod
と合わせて Factoryシリーズ
として紹介されています。
FactoryMethodと似ているデザインパターンで、依存性逆転の原則に則った実装をすることを目的としたデザインパターン(だと思います)。
前提
背景や実装要件はFactoryMethodと基本的に同じです。(少し手を加えてます)
ピザ店を経営している。
ピザ店は複数の支店を持ち、その店舗によって扱っているピザのメニューが異なる。
ピザのメニューは頻繁に変更が想定される。(新メニュー追加や期間限定メニューなど。)
注文を受けた時に、適切なピザクラスを返す仕組みを実装する。
コードで考えてみる
前提をコードで表現してみます。
// -- ピザ クラス --
// レギュラーピザ
class RegularPizza {
open(): void {
console.log("Delivery: RegularPizza");
}
}
// 季節限定ピザ
class SeasonalPizza {
open(): void {
console.log("Delivery: SeasonalPizza");
}
}
// -- 店舗 クラス --
// 店舗クラス
class Store {
order(order: string): RegularPizza | SeasonalPizza {
switch (order) {
case "RegularPizza":
return new RegularPizza();
case "SeasonalPizza":
return new SeasonalPizza();
default:
throw new Error("Nothing in the menu.");
}
}
}
// ------------------
// -- main処理 --
// ------------------
const store = new Store(); // 店舗に来店
const pizza = store.order("SeasonalPizza"); // 季節限定ピザを注文
pizza.open(); // => Delivery: SeasonalPizza
何が問題か?
依存性逆転の原則に反してしまっているのが問題になります。クラス図でいうと以下の様な状態です。
FactoryMethodの時と同じで、上位モジュールが下位モジュールに依存しているのがわかります。
そのため、下位モジュールのPizzaクラスに変更が発生すると、上位モジュールのStoreクラスにも変更が発生してしまうことが想定されます。
これは変更容易性が低い状態であると言え、この程度のコードなら問題ないと思いますが、大きなPJの場合、影響範囲が大きくなるので、改善の必要があります。
Abstract Factoryパターンを使う
実際にデザインパターンを使用して改善してみます。
// -- ピザ クラス --
// ピザのインターフェース
interface IPizza {
open(): void;
}
// レギュラーピザ
class RegularPizza implements IPizza {
open(): void {
console.log("Delivery: RegularPizza");
}
}
// 季節限定ピザ
class SeasonalPizza implements IPizza {
open(): void {
console.log("Delivery: SeasonalPizza");
}
}
// -- Factory クラス --
// Factoryの抽象インターフェース
interface IAbstractFactory {
createPizza(order: string): IPizza;
}
// 具象Factoryクラス
class FactoryA implements IAbstractFactory {
createPizza(order: string): IPizza {
switch (order) {
case "RegularPizza":
return new RegularPizza();
case "SeasonalPizza":
return new SeasonalPizza();
default:
throw new Error("Nothing in the menu.");
}
}
}
// -- 店舗 クラス --
// 店舗クラス
class Store {
factory: IAbstractFactory;
// factoryの定義は初期化時に行う
constructor(factory: IAbstractFactory) {
this.factory = factory;
}
// ピザの生成はFactoryクラスに移譲
order(order: string): IPizza {
return this.factory.createPizza(order);
}
}
// ------------------
// -- main処理 --
// ------------------
const factory = new FactoryA(); // 実行時に初めてどのFactoryが使用されるか決まる
const store = new Store(factory); // 店舗に来店
const order = "SeasonalPizza"; // 注文
const pizza = store.order(order); // ピザが作成される
pizza.open(); // => Delivery: SeasonalPizza
デザインパターンを使用することで、クラス間の関係性が以下の様に変わりました。
上位モジュールのStoreクラスがインターフェースを参照することで、下位モジュールの依存から切り離すことができているのがわかります。
これにより下位モジュールに変更が入っても、上位モジュールへ影響を及ぼすことがなくなりました。
変更時の要所は以下になります。
- 抽象クラス(上位モジュール)は具象クラス(下位モジュール)を参照しない
- 具象クラスの初期化はFactoryクラスが行う
- インターフェースを使用することで、抽象クラスからの具象クラスへの依存を回避する
FactoryMethodとAbstractFactoryの違い
両者似ているので、ここで違いについて改めて整理しておきたいと思います。
(私の解釈の部分もあるので、もし間違っている箇所があればご指摘願います。)
項目 | FactoryMethod | AbstractFactory |
---|---|---|
目的 | 依存性逆転の原則に則った実装をすること | 同じく |
対象 | 抽象クラスから具象クラスへの 関心を分離 する | 上位モジュールから下位モジュールの 依存性を切り離す |
大きな違いは、対象の部分でしょうか。
特にAbstractFactoryの実装内で、FactoryMethodを使用している為、ややこしく感じますね。
(また実際はFactoryMethodの方がより抽象的な課題であることに反し、名前的にはAbstractFactoryの方がより抽象的っぽいので、それも複雑にしている要因だと思います。)
まとめ
前回に引き続き、Factoryシリーズからのデザインパターンでしたが、やはり実際にクラス図を書いてみるのが、分かりやすくて良いですね。(気づき)
Factoryシリーズの記事を書くまでクラス図を書く機会があまりありませんでしたが、最近は複雑なクラス関係はクラス図を書く、という習慣がついた様な気がします。
前回の記事でも少し触れましたがFactoryシリーズは使うのが難しいデザインパターンだな、と感じます。
特にFactoryMethod。それに比べれば、AbstractFactoryは、登場するモジュールも比較的少なく、とっつきやすい感じがあります。が、以前複雑性が増すところはあるので、やはり使い所が重要だと思いました。