この記事は?
オライリー社から出版されている「HeadFirst デザインパターン」を読んだ内容を
理解の整理のためTypescriptで実装してみる内容です。(定期で載せれる様がんばる)
対象となるデザインパターン
参考書の「Factoryパターン」の章で紹介されているのは、以下の3つ。
- Simple Factory
- Gofのデザインパターンじゃないけど、よく見かける実装方法で、本書内では「Simple Factory」というデザインパターン名で紹介されています。
- 内容的にはシンプルなので、今回は省略します。
- AbstractFactory
- Factory Methodと一緒に考えると、ややこしいので別記事でまとめる。
- Factory Method
- 今回の目玉。概要はWiki様参照ください。
Factory Methodについて
前提
参考書ではピザ店のオーダーの仕組みを例に紹介しているので、その例に沿って整理する。
以下はざっくりした設定背景を、独自に解釈したものです。
ピザ店を経営している。
ピザ店は複数の支店を持ち、その店舗によって扱っているピザのメニューが異なる。
ピザの種類毎に必要な材料、レシピは異なる。
注文を受けた時に、適切なピザクラスを返す仕組みを実装する。
コードで考えてみる
まずは前提をコードで表現すると以下のような感じ。
// ------------------
// -- ピザ クラス --
// ------------------
// ピザの抽象クラス
abstract class Pizza {
abstract pizzaName: string;
open(): void {
console.log("Delivery: ", this.pizzaName);
}
}
// 全店舗共通ピザ
class StandardPizza extends Pizza {
pizzaName = "StandardPizza";
}
// 東京店舗限定ピザ
class TokyoPizza extends Pizza {
pizzaName = "TokyoPizza";
}
// 大阪店舗限定ピザ
class OsakaPizza extends Pizza {
pizzaName = "OsakaPizza";
}
// ------------------
// -- 店舗 クラス --
// ------------------
// 東京店舗クラス
class TokyoStore {
order(order: "StandardPizza" | "TokyoPizza"): Pizza {
switch (order) {
case "StandardPizza":
return new StandardPizza();
case "TokyoPizza":
return new TokyoPizza();
}
}
}
// 大阪店舗クラス
class OsakaStore {
order(order: "StandardPizza" | "OsakaPizza"): Pizza {
switch (order) {
case "StandardPizza":
return new StandardPizza();
case "OsakaPizza":
return new OsakaPizza();
}
}
}
// ------------------
// -- main処理 --
// ------------------
const store = new TokyoStore(); // 東京店舗に来店
const pizza = store.order("TokyoPizza"); // 東京限定ピザを注文
export default pizza.open(); // => Delivery: TokyoPizza
クラス図にするとこんな感じ
FactoryMethodパターンは「依存性逆転の原則」を遵守するためにあるようなデザインパターンなので(独自解釈)、クラス間の依存関係を考えると意図を把握しやすいです。
ポイントは依存性の方向。上位モジュールが下位モジュールに依存してしまっているのが問題です。
このままだと下位モジュールの変更が、上位モジュールに影響を与えてしまうので、将来的に変更が多く予測されるモジュールの場合、改善した方が良さそうです。
参考書では以下が依存性逆転の原則に違反してないかチェックする上での指針になると紹介されてます。
・具象クラスへの参照を持つ変数を持たない。
・具象クラスからクラスを継承しない。
・基底クラスの実装済みのメソッドをオーバーライドしない。
全てを守ろうとするとコードが書けなくなるので、対象となるモジュールの変更の可能性を考慮する必要がある。変更の可能性が高いモジュールにおいて、その変更をカプセル化するテクニックとしてFactoryMethodがある。
Factory Methodパターンを使う
ここで登場するのが「Factory Method」パターン。依存性逆転の原則に問題がある場合に役立つデザインパターンです。さっそく改修していきましょう。
改修版コード
// ----------------------------
// -- ピザ クラス(ここは同じ) --
// ----------------------------
// ピザの抽象クラス
abstract class Pizza {
abstract pizzaName: string;
open(): void {
console.log("Delivery: ", this.pizzaName);
}
}
// 全店舗共通ピザ
class StandardPizza extends Pizza {
pizzaName = "StandardPizza";
}
// 東京店舗限定ピザ
class TokyoPizza extends Pizza {
pizzaName = "TokyoPizza";
}
// 大阪店舗限定ピザ
class OsakaPizza extends Pizza {
pizzaName = "OsakaPizza";
}
// ------------------------
// -- Factory クラス(新規) --
// ------------------------
// ポイント①: Factoryクラスのインターフェース
// Factoryクラスが実行するメソッドはインターフェースとして宣言
interface PizzaFactory {
makePizza(order: string): Pizza;
}
// 東京店舗のピザFactory
class TokyoPizzaFactory implements PizzaFactory {
// Pizzaクラスを返す
makePizza(order: "StandardPizza" | "TokyoPizza"): Pizza {
switch (order) {
case "StandardPizza":
return new StandardPizza();
case "TokyoPizza":
return new TokyoPizza();
}
}
}
// 大阪店舗のピザFactory
class OsakaPizzaFactory implements PizzaFactory {
// Pizzaクラスを返す
makePizza(order: "StandardPizza" | "OsakaPizza"): Pizza {
switch (order) {
case "StandardPizza":
return new StandardPizza();
case "OsakaPizza":
return new OsakaPizza();
}
}
}
// ----------------------
// -- 店舗 クラス(改修) --
// ----------------------
// 抽象店舗クラス
abstract class Store {
// ポイント②: factory methodの定義
// 下位モジュールへの依存箇所を、サブクラスに委ねる
// Storeクラスはサブクラスが実装されるまで、何のモジュールが返るか責任を負わない
abstract getPizzaFactory(): PizzaFactory;
order(order: string): Pizza {
const factory = this.getPizzaFactory();
return factory.makePizza(order);
}
}
// 東京店舗クラス
class TokyoStore extends Store {
factory: PizzaFactory;
// ポイント③: コンストラクタでfactoryを受けることで依存性を避ける
constructor(factory: PizzaFactory) {
super();
this.factory = factory;
}
getPizzaFactory() {
return this.factory;
}
}
// ------------------
// -- main処理 --
// ------------------
const store = new TokyoStore(new TokyoPizzaFactory()); // 東京店舗に来店
const pizza = store.order("TokyoPizza"); // 東京限定ピザを注文
export default pizza.open(); // => Delivery: TokyoPizza
見た目的には、StoreクラスにあったPizzaクラスのインスタンス化の部分が、Factoryクラスに移譲されただけに見えます。
実際その通りで、処理の流れ自体には大きな変更はなく、ぱっと見何の違いがあるのか、必要性があるのか、とても分かりづらいです。(個人的にはこれがFactoryMethodが理解しづらい理由な気がします。)
FactoryMethodは、依存性逆転の原則を正しく実装するためのデザインパターンだと感じます(個人解釈)。処理のフローは変わらないですが、以下のクラス図を見ると依存関係が変わり、より変更に強い実装になっているのが分かると思います。
クラス図(FactoryMethod修正版)
ここで重要なのは 「上位モジュールが下位モジュールに依存してない」 ことです。
下位モジュールへの依存性を回避することで、下位モジュールの変更をカプセル化することができてると思います。
(FactoryとPizzaクラスは共に下位モジュールなので、そこの依存性は問題ない、、、という理解です。)
まとめ
正直、参考書を読んだだけでは全然よくわらん FactoryMethodでした
個人的な感想ですが、オライリーの参考書だけでは漠然としすぎてて完全理解は不可能だと思いました。
(記事作成に際し、インターネットでかなり追加調査しました。)
勉強してみて感じたことですが、FactoryMethodは他のデザインパターンと比べると使い所が難しそうですね。
小さな依存性逆転は、実際のところ現場ではちょいちょい見ることあると思っていて、どこまでそれを修正するかは、そのシステムの規模や、機能の性質、そしてPJチームのレベル等を検討して決める必要があると感じました。
なぜならば、FactoryMethodを用いると、途端に関係性が複雑になり、コード量が多くなるからです。
場合によっては、シンプル性を優先し、下位モジュールへの依存を許容した方が良いケースもりそうだなー、って思いました。