🍣

Head First デザインパターン オブジェクトの装飾

2024/10/20に公開

オブジェクトの装飾

  • 継承の典型的な使い過ぎを再検証する
  • コンポジションを使って実行時にクラスを装飾する方法を学ぶ
    • 基盤となるクラスのコードに変更を加えることなく、オブジェクトに新しい責務を与えられる

最初の状態

  • 珈琲の種類・トッピングの全ての組み合わせについて派生クラスを作成
  • それぞれのクラスのcostメソッドをオーバーライドすることで、価格を算出

最初のクラス図

対応案1

対応案1クラス図

  • コンディメントの価格改定があると、スーパークラスのコードを変更する必要がある
  • 新しいコンディメントが追加されると、スーパークラスに新しいメソッドを追加し、costメソッドを変更する必要がある
  • 新しい飲み物が追加され、その飲み物に不適切なコンディメントがあった場合、対応できない
  • ダブルモカを頼みたい場合に対応できない

設計原則

  • open-closedの原則
    • クラスを拡張に対しては開かれた状態にするべきだが、変更に対して閉じた状態にする
    • 既存コードは変更せずに、クラスを簡単に拡張して新しい振る舞いを組み込めるようにする

素朴な疑問に答えます

  • 「拡張に対しては開いていて、変更に対しては閉じている」は矛盾しているように思える
    • 基盤となるコードは変更せずに、システムを拡張できるOOテクニックがいくつかある
      • たとえば、2章のObserverパターンは、新しいオブザーバを追加しても、サブジェクトは何も変更しなくて良い
  • Ovserverパターン以外で、一般的にはどのように行うのか?
    • 多くのパターンでは、拡張する方法を用意することで、基盤コードの変更が必要ない設計になっている
    • この章では、Decoratorパターンを利用する
  • 全ての部分にオープン/クローズドの原則を適用したい
    • 通常は不可能。また意味もない。
    • こうした設計(抽象化)を導入すると、コードが複雑になる
    • 必要な部分を見極める必要がある
  • どうすれば必要な部分がわかるのか?
    • OOシステム設計の経験を積む
    • 対象分野の理解する
    • 多くの設計パターンを学ぶ

デコレータパターンの特徴

  • デコレータは装飾するオブジェクトと同じスーパータイプを持つ
  • 複数のデコレータを使用してオブジェクトをラップできる
  • デコレータは装飾するオブジェクトと同じスーパータイプを持つので、基のオブジェクトの代わりに装飾されたオブジェクトを使用できる
  • デコレータは装飾するオブジェクトに委譲する前後どちらかまたは両方で独自の振る舞いを追加して、残りの処理を実行する
  • いつでもオブジェクトを修飾できるため、任意の数のデコレータを使って実行時に動的にオブジェクトを装飾できる

デコレータパターンの定義

Dekoratorパターンはオブジェクトに追加の責務を動的に付与する。デコレータは、サブクラス化の代替となる、柔軟な機能拡張手段を備えている。

デコレータパターンのクラス図

Go言語の場合、抽象クラスがないので書籍のようにDecorator抽象クラスを作るかどうかは場合による。この例だと、Decorator抽象クラスの役割は、Componentへの参照を持つことだけなので、作成しても冗長になるだけなので作成しない。※ここは好みもあるかも。

デコレータパターンのクラス図

コード

component.go
// 飲み物ベース
type BevarageBase struct {
  description string
}

func (b BevarageBase) getDescription() string {
  return b.description
}

// ブレンド
type HouseBlend struct {
  BevarageBase
}

func (h HouseBlend) cost() float64 {
  return 0.89
}

// エスプレッソ
type Espresso struct {
  BevarageBase
}

func (e Espresso) cost() float64 {
  return 1.99
}
decorator.go
// ミルク
type Milk struct {
  bevarage Bevarage
}

func (m Milk) cost() float64 {
  return m.bevarage.cost() + 0.1
}

func (m Milk) getDescription() string {
  return m.bevarage.getDescription() + ",ミルク"
}

// モカ
type Mocha struct {
  bevarage Bevarage
}

func (m Mocha) cost() float64 {
  return m.bevarage.cost() + 0.2
}

func (m Mocha) getDescription() string {
  return m.bevarage.getDescription() + ",モカ"
}

// 豆乳
type Soy struct {
  bevarage Bevarage
}

func (s Soy) cost() float64 {
  return s.bevarage.cost() + 0.15
}

func (s Soy) getDescription() string {
  return s.bevarage.getDescription() + ",ソイ"
}

// ホイップ
type Whip struct {
  bevarage Bevarage
}

func (w Whip) cost() float64 {
  return w.bevarage.cost() + 0.1
}

func (w Whip) getDescription() string {
  return w.bevarage.getDescription() + ",ホイップ"
}
main.go
func main() {
  // ベースを選択
  var b Bevarage = HouseBlend{BevarageBase: BevarageBase{description: "HouseBlend"}}
  // 豆乳を追加
  b = Soy{bevarage: b}
  // モカを追加
  b = Mocha{bevarage: b}
  // モカを追加
  b = Mocha{bevarage: b}
  // ホイップを追加
  b = Whip{bevarage: b}

  fmt.Printf("注文は、%s\n", b.getDescription())
  fmt.Printf("値段は、$%.2f\n", b.cost())
}

// 飲み物インターフェス
type Bevarage interface {
  getDescription() string
  cost() float64
}

素朴な疑問に答えます

  • 特定の具象コンポーネント(たとえば、HouseBulend)であることを確認してから割引を行うような場合は、どうするのか?
    • できない。設計を考え直した方が良いかも。
  • デコレータを沢山使う場合の「取り違え」はどうするか?
    • オブジェクト生成のためのパターンを使うことでカバーする
  • デコレータはデコレータチェーン内の他のデコレータについて知ることはできるか?
    • デコレータはラップするオブジェクトに振る舞いを追加することが目的
    • デコレータチェーン内の複数のレイヤを見るのは、デコレータの本来の目的を超えている

自分で考えてみよう

  • サイズが追加された
  • サイズごとにコンディメントの値段も変更したい

変更後のcomponent.goのコードは以下

component.go
+ const (
+   tall = iota
+   grande
+   venti
+ )

+ type Size int

// 飲み物ベース
type BevarageBase struct {
  description string
+ size Size
}

+ func (b *BevarageBase) setSize(s Size) {
+    b.size = s
+ }
+ func (b BevarageBase) getSize() Size {
+    return b.size
+ }
func (b BevarageBase) getDescription() string {
  return b.description
}

// ブレンド
type HouseBlend struct {
- BevarageBase
+ *BevarageBase
}

変更後のdocorator.go、中間クラスCondimentBaseを追加した

decorator.go
+ // コンディメントベース
+ type CondimentBase struct {
+    bevarage Bevarage
+ }

+ func (b *CondimentBase) setSize(s Size) {
+	 b.bevarage.setSize(s)
+ }
+ func (b CondimentBase) getSize() Size {
+    return b.bevarage.getSize()
+ }

// ミルク
type Milk struct {
- bevarage Bevarage
+ CondimentBase
}

+ func (m Milk) cost() float64 {
+    if c := m.bevarage.getSize(); c == tall {
+        return m.bevarage.cost() + 0.1
+    } else if c == grande {
+        return m.bevarage.cost() + 0.15
+    } else {
+        return m.bevarage.cost() + 0.2
+    }
+ }

func (m Milk) getDescription() string {
  return m.bevarage.getDescription() + ",ミルク"
}
  • size毎に派生クラスを作るのかと思ったが、愚直に条件分岐していた。sizeが増えたらどうするのだろう
  • ただし、size毎に派生クラスを作ってしまうと、別の軸が出てきた時にも派生クラスが必要になる
  • そうすると派生クラスだらけになってしまう

デコレータへのインタビュー

  • デコレータは便利だが欠点もある
  • デコレータが増えすぎて収拾が付かなくなってしまう場合がある
  • さらには、特定のデコレータ型に依存したコードがかかる場合がある
  • デコレータの利用方法が理解されていないことが問題
  • デコレータを複数使ったオブジェクトを組み立てるのが煩雑であることも問題の一つ
    • Factory、Builderパターンを導入する

Discussion