NAKKA-Kの技術ブログ

技術に関する知見や考え方などを投稿します。

「HeadFirstデザインパターン」のObserverパターンをGo言語で実装してみた

「HeadFirstデザインパターン」ではJavaを用いてデザインパターンの実装が解説されています。 これらのデザインパターンをGo言語で実験的に設計し直した実装を紹介します。

前回はStrategyパターンを実装しました。

nakka-k.hatenablog.com

Observerパターンとは

簡単な概要だけを説明すると、

親となるインターフェースと子となるインターフェースがあり、子は親のグループに入ったり出たりできます。 そして、親はグループ内の子に対して任意のタイミングでメッセージを送ることができるパターンです。

本に出てくるObserverパターンのイメージ
本に出てくるObserverパターンのイメージ

実装

今回のコードは全てGitHubのリポジトリにありますので、全体のソースコードはそちらからご覧ください。

まずはObserverの親となるインターフェースと子となるインターフェースを定義します。

package main

// 親となるインターフェース
type Subject interface {
    RegisterObserver(o Observer)
    RemoveObserver(o Observer)
    NotifyObservers()
}

// 子となるインターフェース
type Observer interface {
    Update(temperature, humidity, pressure float64)
}

では次にSubjectの実装を見ていきましょう。

package main

// Subjectインターフェースの実装
// グループに入った子を保存するようのObserver配列を持つ必要があります。
type WeatherData struct {
    observers   []Observer
    temperature float64
    humidity    float64
    pressure    float64
}

// コンストラクタ
func NewWeatherData() *WeatherData {
    wd := &WeatherData{}

    wd.observers = []Observer{}
    return wd
}

func (wd *WeatherData) RegisterObserver(o Observer) {
    wd.observers = append(wd.observers, o)
}

// (Go言語にはスライスの要素削除関数はないので自前で実装する必要があります)
// (実戦で書くときはもう少しちゃんと書きましょう)
func (wd *WeatherData) RemoveObserver(o Observer) {
    observers := make([]Observer, len(wd.observers)-1, len(wd.observers)-1)
    for _, obs := range wd.observers {
        if obs != o {
            observers = append(observers, obs)
        }
    }
    wd.observers = observers
}

// ここでグループに入っている子に対してメッセージを一斉送信します
func (wd *WeatherData) NotifyObservers() {
    for _, obs := range wd.observers {
        obs.Update(wd.temperature, wd.humidity, wd.pressure)
    }
}

func (wd *WeatherData) MeasurementsChanged() {
    wd.NotifyObservers()
}

func (wd *WeatherData) SetMeasurements(temperature, humidity, pressure float64) {
    wd.temperature = temperature
    wd.humidity = humidity
    wd.pressure = pressure
    wd.MeasurementsChanged()
}

func (wd *WeatherData) GetTemperature() float64 {
    return wd.temperature
}

func (wd *WeatherData) GetHumidity() float64 {
    return wd.humidity
}

func (wd *WeatherData) GetPressure() float64 {
    return wd.pressure
}

ここで重要なのが、構造体にObserver配列を持っている点です。 その配列に登録されたObserverたちを保存しておく必要があります。 この配列を管理することがSubjectの大きな仕事になります。

ついでにGo言語のインターフェースの仕様について補足しておきます。 Observer等のインターフェースは値渡しになっているように見えますが、内部にポインタを持っているので構造体とは違って参照渡しになっています。

次はObserverインターフェースの実装を見ていきましょう。

package main

import "fmt"

// Observerインターフェースの実装
// ここではWeatherData(Subjectインターフェース)の参照を持っていますが、実際は絶対に必要というわけではありません。
// しかし子の方から自由にグループ離脱ができて便利なため、親の参照を持つ場合が多いです。
type ForecastDisplay struct {
    currentPressure float64
    lastPressure    float64
    weatherData     *WeatherData
}

// コンストラクタ
// メンバ変数の初期値はコンストラクタ内でしています。
// そして自分自身をSubjectのグループに登録しています。
func NewForecastDisplay(weatherData *WeatherData) *ForecastDisplay {
    fd := &ForecastDisplay{}
    fd.currentPressure = 29.92

    fd.weatherData = weatherData
    weatherData.RegisterObserver(fd)
    return fd
}

func (fd *ForecastDisplay) Update(temperature, humidity, pressure float64) {
    fd.lastPressure = fd.currentPressure
    fd.currentPressure = pressure

    fd.Display()
}

func (fd *ForecastDisplay) Display() {
    fmt.Print("Forecast: ")
    if fd.currentPressure > fd.lastPressure {
        fmt.Println("Improving weather on the way!")
    } else if fd.currentPressure == fd.lastPressure {
        fmt.Println("More of the same")
    } else if fd.currentPressure < fd.lastPressure {
        fmt.Println("Watch out for cooler, rainy weather")
    }
}

ここで気をつけることはほとんどありませんが、自分が登録したグループの親であるweatherDataの参照を保存している点は要注意です。 ここは求められる仕様によって変えていっても大丈夫だとは思いますが、多くの場合は持っていた方が良いと思います。 なぜならコードが単純になりやすいからです。

コンストラクタについて補足すると、Javaと違ってオブジェクトが全て参照渡しではないので明示的にWeatherDataのポインタを渡すようにしています。 そして自分自身をSubjectのグループに追加する処理も走らせています。

まとめ

Observerパターンの一番の重要点は、情報が一方向にしか流れず、双方向に同期を取る必要がない点です。 Subjectが情報を一元管理し、変更した時点で子に対して通知するだけで後はよしなに、という簡素な関係です。

もしこれが同じ階層のオブジェクト同士でいろんな情報を相互にやりとりしていれば、情報の扱いが非常に面倒なことになってしまいます。

そしてもちろん、SubjectインターフェースとObserverインターフェースに対して実装することでコードを綺麗に保つことも重要です。 HeadFirst本でも書かれていますが、JavaにはObserverパターンを実装するためのパッケージが存在します。 これはインターフェースではなく、継承という形で実装されます。 そうなると拡張性や再利用生が失われかねませんので、やはりインターフェースで実装できるときはインターフェースで実装するようにした方が多くの場合良いでしょう。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

この本のデザインパターンを順次試していく予定なので、次回もどうぞご覧ください。