🙆

Head First デザインパターン OOの利点を活用した構築

2024/10/22に公開

OOの利点を活用した構築

  • 「new」を見たら具象と考える

「new」の何が問題?

  • newには特に問題はない
  • 問題は、変更
  • 新しい具象クラスを作成すると、そのインスタンスを生成するコードを追加する必要がある
  • この変更を他の不変な部分と分離する

変化する部分を特定する

ピザ屋の最初のコード

func orderPizza() Pizza {
  pizza := Pizza{}

  pizza.prepare()
  pizza.bake()
  pizza.cut()
  pizza.box()
  return pizza
}

複数種類のピザが必要。Goの場合、抽象クラスがないのでこの時点でインターフェイスが必要。

func orderPizza(pType string) Pizza {
  var pizza Pizza

  if pType == "チーズ" {
    pizza = CheesePizza{}
  } else if pType == "ギリシャ" {
    pizza = GreekPizza{}
  } else {
    pizza = PepperroniPizza{}
  }

  pizza.prepare()
  pizza.bake()
  pizza.cut()
  pizza.box()
  return pizza
}

type Pizza interface {
  prepare()
  bake()
  cut()
  box()
}

さらに、ピザの種類を増やしたい。また、人気のないピザを削除したい。

単純なファクトリーの導入

// ピザファクトリー
type SimplePizzaFactory struct{}

func (s SimplePizzaFactory) createPizza(pType string) Pizza {
  if pType == "チーズ" {
    return CheesePizza{}
  } else if pType == "ペパロニ" {
    return PepperroniPizza{}
  } else if pType == "アサリ" {
    return ClamPizza{}
  } else if pType == "野菜" {
    return VaggiePizza{}
  } else {
    return PepperroniPizza{}
  }
}

func orderPizza(pType string) Pizza {
  factory := SimplePizzaFactory{}
  var pizza Pizza = factory.createPizza(pType)

  pizza.prepare()
  pizza.bake()
  pizza.cut()
  pizza.box()
  return pizza
}

素朴な疑問に答えます

  • 別のオブジェクトに問題を押し付けているだけじゃないの?
    • ファクトリは複数のクライアントを持てる
      • ピザの追加削除の影響を1箇所に集約できる
    • クライアントコードから具象クラスのインスタンスを取り除くことができた
  • ファクトリを静的メソッドで実装したパターンを見たことがある
    • そのようなパターンは、「スタティックファクトリ」と呼ぶ
    • メリット:ファクトリをインスタンス化しなくてよい
    • デメリット:多態性を活用できない

Go言語で、スタティックファクトリをやる場合は、単純に関数で定義する。多態性を実現したい場合には、関数としての型を定義するか、関数自体をインターフェイスにすることで対応できる。という風に考えると、単純なファクトリ(引数で分岐様なもの)は関数で定義した方がコードがシンプルにできそう。

Simple Factoryの定義

  • Simple Factoryは、デザインパターンではない
  • イディオムのようなもの

ピザ店をフランチャイズ化する

  • 異なる種類のピザ(ニューヨーク、シカゴ、カリフォルニアなど)を提供したい
    • 同じ野菜ピザでも、ニューヨーク、シカゴ、カリフォルニアスタイルのピザがある
  • スタイルは店ごとに決定する

それぞれのスタイルのファクトリーを作る方法だと、ファクトリとPizzaStoreが結びついているためにうまくいかない。

ピザ店用のフレームワーク

個人的な感想

まず、Factory Methodパターンの定義は、一般的に混乱している模様。Webの記事を検索してみると、以下の定義にはまらないモノが多数ヒットする。

wiki Factory Method パターン

ウィキペディアにもあるように、ファクトリにあたる部分を、抽象メソッドとしてサブクラスで上書きさせるもの、がFactory Factory Methodパターン。※Template Methodパターンの特殊なもの

Go言語だと、オーバーライドがないので、このパターンは使いづらい

pizzaStore.go
type PizzaStore struct {
  createPizza func(pType string) Pizza
}

func (ps PizzaStore) orderPizza(pType string) Pizza {
  var pizza Pizza = ps.createPizza(pType)

  pizza.prepare()
  pizza.bake()
  pizza.cut()
  pizza.box()
  return pizza
}
  • シングルメソッドのファクトリなら、これが一番シンプルな方法だと思う
  • 結構無理やりになるが、↓の製作者と製品の紐づけも、PizzaStoreクラスの埋め込みと、その構造体のNewを使ったメソッド紐づけで対応できる
    • これをやる必要があるかは疑問

ただし、Factory Methodパターンの利点というかわかりやすさは、PizzaStoreのどの具象クラスがインスタンス化されているかがわかれば、どのPizza具象クラスが使われている(可能性があるか)がわかる、という可読性の観点だと思われるのでそういう意味でメリットがあるかもしれない。

製作者と製品を並列にとらえる

並列

FactoryMethodパターンの定義

  • オブジェクトを生成するためのインタフェースを定義する
  • どのクラスをインスタンス化するかについてはサブクラスに決定させる

オブジェクトの依存関係を考察する

  • ファクトリを使わない場合、PizzaStoreはすべてのPizzaの具象クラスに依存することになる
  • 新しいPizzaが増えるたびに依存が増える

設計原則(依存関係反転の原則)

  • 抽象に依存する
  • 具象クラスに依存指定はいけない

この原則は、「実装に対してではなくインターフェースに対してプログラミングする」という原則に似ている。しかし、この依存関係反転の原則の方が抽象化についてされに強く主張している。

  • 高水準のコンポーネントは低水準のコンポーネントに依存すべきではない
  • どちらも抽象に依存すべき

原則を適用する

PizzaStoreの例で考える。

  • Factory Methodパターンを適用する
    • PizzaStoreは、Pizza抽象クラスに依存する
    • 具象ピザクラスは、Pizza抽象クラスに依存する
      • 抽象Pizzaクラスにインターフェースを決定されているため、依存しているといえる

考え方を反転させる

  • PizzaStoreを実装する場合、最初に何を考えるか?
    • CheesePizza,VeggiePizzaなどの様々な種類のピザを作る必要がある
  • つまり、最上位から初めて具象クラスまで下がっていく
  • しかし、PizzaStoreは、具象ピザ型について知ってほしくない
  • ここで、ピザから考えて何を抽象化できるか考える
    • CheesePizza,VeggiePizzaなどはすべてPizzaだからPizzaインタフェースを共有するはず
  • ここでPizzaStoreの設計についてもう一度考える
    • ピザの抽象化ができたので具象ピザクラスを気にせずにPizzaStoreを設計できる

個人的な感想

  • 現実の設計問題としては、この例のように始めから同じインタフェースにきれいに収まることがわかっていないケースも多々ある
  • 逆にクライアント側から発想して、どういうインタフェースがあれば複数の具象クラスを1つのインタフェースにまとめられるのか、を考える必要がある
    • たとえば、orderPizzaでなくて、orderDishで、パスタやサラダが入ってくるような

この原則に従うために役立つ指針

  • 具象クラスへの参照持つ変数を持たない
    • ファクトリを使って回避する
  • 具象クラスからクラスを継承しない
    • インタフェースや抽象クラスなどの抽象を継承する
  • 既定クラスの実装済みのメソッドをオーバーライドしない
    • オーバーライドする≒抽象ではない
    • 実装済みのメソッドはすべてのサブクラスが共有すべき

これらすべてを「常に完全に守るべき」ということではなく、違反している場合に「違反すべきもっともな理由があるのか?」を検討する。

Abstract Factoryパターンの定義

具象クラスを指定せずに、一連の関連オブジェクトや依存オブジェクトを生成するためのインタフェースを提供する

個人的な感想

「一連の」 という言葉がポイント。Factory Methodパターンと違い複数のメソッドの振る舞いを変更したい場合に使う

今一つ、Factory MethodとAbstract Factoryの使い分けがはっきりと理解できない。

  • Factory Method
    • creator側の1つのメソッドのオーバーライドで実現
    • 製作者と製品を並列に紐づけで管理する
      • 製作者と製品の関係は固定
  • Abstract Factory
    • Factoryの複数のメソッドのオーバーライドで実現
    • 工場と製品を並列に紐づけで管理する
    • 製作者は工場を外から受け取る

感想としては、Factory MethodでやりたいことをAbstract Factoryで実現することのできるし、その逆もできる。

  • Factory Methodは、過度な複雑さを導入しないことに重きを置いている
  • Abstract Factoryは、柔軟性を上げることに重きを置いている

ということかと思う。

いずれにせよ、Go言語ではAbstract Factory的な手法をつかうことになる。

Discussion