🎉

Head First デザインパターン オブジェクトを事情通に

2024/10/17に公開

初めの状態

type WeatherData struct {...}

func (w WeatherData) getTemperature() float64 {...}
func (w WeatherData) getHumidity() float64 {...}
func (w WeatherData) getPressure() float64 {...}
func (w WeatherData) measurementsChanged() {
    // 以下にコードを書く
}
  • WeatherDataオブジェクトは変化があるたびに、measurementsChangedメソッドが呼び出される
  • measurementsChangedが呼び出される度に、3つの画面の表示内容を更新したい
  • 将来的に、3つだけでなく任意の数の画面の更新に対応したい

気象観測所に関する最初の間違った実装

type WeatherData struct {...}

func (w WeatherData) measurementsChanged() {
    temp := getTemperature()
    humidity := getHumidity()
    pressure := getPressure()

    currentConditionsDisplay.update(temp, humidity, pressure)
    statisticsDisplay.update(temp, humidity, pressure)
    forecastDisplay.update(temp, humidity, pressure)
}
// 他のWeatherDataのメソッド

Observerパターンの定義

Observerパターンでは、あるオブジェクトの状態が変化すると、そのオブジェクトに依存しているすべてのオブジェクトに自動的に通知され更新されるようにする、オブジェクト間の1対多の依存関係が定義されている

  • Observerパターンでは、一連のオブジェクトの間に1対多の関係が定義されている
  • あるオブジェクトの状態が変化すると、そのオブジェクトに依存するすべてのオブジェクトに通知される

Observerパターンの定義:クラス図

Observerパターンのクラス図

疎結合

  • オブジェクトが他のオブジェクトに依存しすぎている場合に密結合という
  • すべてのオブジェクトはなんらか他のオブジェクトに依存する
  • 疎結合のオブジェクトは他のオブジェクトの詳細についてあまり知らないか、あまり気にしない
  • 他のオブジェクトについてあまり知らないほうが、変更にうまく対処する設計ができる

疎結合の威力

Observerパターンは疎結合の良い例

  • サブジェクトがオブザーバについて知っていることは、オブザーバがある特定のインターフェースを実装していることだけ
  • 新しいオブザーバをいつでも追加できる
    • オブザーバはいつでも交換可能なので、動的にオブザーバを追加削除できる
  • 新しい種類のオブザーバを追加するのに、サブジェクトを変更する必要は全くない
    • Observerインターフェースを実装しているクラスのオブジェクトであればなんでもよい
  • サブジェクトやオブザーバをそれぞれ独立して再利用できる
  • サブジェクトまたはオブザーバのどちらかを変更しても、他方に影響を与えない
    • それぞれのインターフェースを満たしている限り、変更の影響が及ぶことはない

設計原則

  • 相互にやり取りを行うオブジェクトの間には、疎結合設計を使う
    • 疎結合設計はオブジェクト間の相互依存を最小限にするため、変更に対応できる柔軟なOOシステムを構築できる

以下、実装

subjectがObserverインターフェースを使うので、Observerインターフェースはここに定義する

subject.go
// Observerインターフェース
type Observer interface {
  update(t float64, h float64, p float64)
}

// WeatherData
type WeatherData struct {
  temperature float64
  humidity    float64
  pressure    float64
  Observers   map[Observer]bool
}

func (w WeatherData) getTemperature() float64 { return w.temperature }
func (w WeatherData) getHumidity() float64    { return w.humidity }
func (w WeatherData) getPressure() float64    { return w.pressure }
func (w *WeatherData) measurementsChanged() {
  // 以下にコードを書く
  w.notifyObserver()
}

func (w *WeatherData) registerObserver(o Observer) {
  w.Observers[o] = true
}

func (w *WeatherData) removeObserver(o Observer) {
  delete(w.Observers, o)
}

func (w WeatherData) notifyObserver() {
  t := w.getTemperature()
  h := w.getHumidity()
  p := w.getPressure()
  for k := range w.Observers {
    k.update(t, h, p)
  }
}

// 観測所から通知が来たことを偽造するためのメソッド
func (w *WeatherData) setMeasurements(t float64, h float64, p float64) {
  w.temperature = t
  w.humidity = h
  w.pressure = p
  w.measurementsChanged()
}

同様にobserverがSubjectインターフェースを使うので、ここに定義する

※説明の都合なのかわからないが、Subjectインターフェースは使っていない点に注意。※インターフェースを使ってしまうと、次のステップで混乱します。

observer.go
// Subjectインターフェース
type Subject interface {
  registerObserver(o Observer)
  removeObserver(o Observer)
  notifyObserver()
}

// CurrentConditionsDisplay
func NewCurrentConditionsDisplay(w *WeatherData) CurrentConditionsDisplay {
  c := CurrentConditionsDisplay{subject: w}
  c.subject.registerObserver(&c)
  return c
}

type CurrentConditionsDisplay struct {
  temperature float64
  humidity    float64
  subject     *WeatherData
}

func (d *CurrentConditionsDisplay) update(t float64, h float64, p float64) {
  d.temperature = t
  d.humidity = h
  d.display()
}

func (d CurrentConditionsDisplay) display() {
  fmt.Printf("現在の気象状況:温度%2.0f度(華氏) 湿度%2.0f%%\n", d.temperature, d.humidity)
}

テスト的に実行

main.go
func main() {
  w := WeatherData{Observers: make(map[Observer]bool)}
  c := NewCurrentConditionsDisplay(&w)
  c.display() // Goは、定義した変数を利用しないとエラーになるため、とりあえず呼び出しているだけ

  w.setMeasurements(80, 65, 30.4)
  w.setMeasurements(82, 70, 29.2)
  w.setMeasurements(78, 90, 29.2)
}

実行結果

現在の気象状況:温度 0度(華氏) 湿度 0%
現在の気象状況:温度80度(華氏) 湿度65%
現在の気象状況:温度82度(華氏) 湿度70%
現在の気象状況:温度78度(華氏) 湿度90%

熱指数の追加

新しく熱指数を計算して表示する構造体を追加

observer.go
// 熱指数
func NewHeatIndexDisplay(w *WeatherData) HeatIndexDisplay {
  c := HeatIndexDisplay{subject: w}
  c.subject.registerObserver(&c)
  return c
}
type HeatIndexDisplay struct {
  heatIndex float64
  subject   *WeatherData
}

func (d *HeatIndexDisplay) update(t float64, h float64, p float64) {
  d.heatIndex = d.computeHeatIndex(t, h)
  d.display()
}

func (d HeatIndexDisplay) display() {
  fmt.Printf("Heat index is %.5f\n", d.heatIndex)
}

func (d HeatIndexDisplay) computeHeatIndex(t float64, rh float64) float64 {
  index := ((16.923 + (0.185212 * t) + (5.37941 * rh) - (0.100254 * t * rh) +
    (0.00941695 * (t * t)) + (0.00728898 * (rh * rh)) +
    (0.000345372 * (t * t * rh)) - (0.000814971 * (t * rh * rh)) +
    (0.0000102102 * (t * t * rh * rh)) - (0.000038646 * (t * t * t)) + (0.0000291583 *
    (rh * rh * rh)) + (0.00000142721 * (t * t * t * rh)) +
    (0.000000197483 * (t * rh * rh * rh)) - (0.0000000218429 * (t * t * t * rh * rh)) +
    0.000000000843296*(t*t*rh*rh*rh)) -
    (0.0000000000481975 * (t * t * t * rh * rh * rh)))
  return index
}

実行コードに熱指数を追加

main.go
func main() {
  w := WeatherData{Observers: make(map[Observer]bool)}
  c := NewCurrentConditionsDisplay(&w)
+  h := NewHeatIndexDisplay(&w)
  c.display() // Goは、定義した変数を利用しないとエラーになるため、とりあえず呼び出しているだけ
+  h.display() // Goは、定義した変数を利用しないとエラーになるため、とりあえず呼び出しているだけ

  w.setMeasurements(80, 65, 30.4)
  w.setMeasurements(82, 70, 29.2)
  w.setMeasurements(78, 90, 29.2)
}

実行結果

現在の気象状況:温度 0度(華氏) 湿度 0%
Heat index is 0.00000
現在の気象状況:温度80度(華氏) 湿度65%
Heat index is 82.95535
現在の気象状況:温度82度(華氏) 湿度70%
Heat index is 86.90123
現在の気象状況:温度78度(華氏) 湿度90%
Heat index is 83.64967

プル型に修正する

※プル型というと、Observerのタイミングで値を取りに行くようなイメージがあるが、ここではタイミングはSubject側が制御し、Observer側が、必要な値のみ取得するように修正する。

subject.go
// Observerインターフェース
type Observer interface {
-  update(t float64, h float64, p float64)
+  update()
}

func (w WeatherData) notifyObserver() {
-  t := w.getTemperature()
-  h := w.getHumidity()
-  p := w.getPressure()
  for k := range w.Observers {
-    k.update(t, h, p)
+    k.update()
  }
}
observer.go
- func (d *CurrentConditionsDisplay) update(t float64, h float64, p float64) {
+ func (d *CurrentConditionsDisplay) update() {
-  d.temperature = t
-  d.humidity = h
+ d.temperature = d.subject.getTemperature()
+ d.humidity = d.subject.getHumidity()
  d.display()
}

個人的な感想

  • Subjectインターフェースを使っていない
  • WeatherDataクラスのgetterを使っている(密結合)

これらの点に全く触れられていないのが非常にモヤモヤする。

Discussion