islands5 blog

日々起こったことを共有したり、思ったこと、勉強したことを書いていくブログ。普段はRailsやAWSを活用したWeb系の開発をやってます。

【デザインパターン】RubyでObserverパターン

この記事は『Head Firstデザインパターン』の内容を自分なりにメモしたものです。 サンプルコードをrubyで書き換えたりしているので、玄人の方はぜひコメントを!! 僕のように初級者で気になった方はぜひ書籍の方を確認してみてください^^

気象情報の変更を受けて表示を変更する

構成はこんな感じ

f:id:fiveislands:20160814224852j:plain

あらかじめ気象情報を提供してくれるAPIがあって、そこにWeatherDataオブジェクトがアクセスして表示を更新する

最初のコード

class WeatherData
  def measurements_changed
    temp = get_temperature
    humidity = get_humidity
    pressure = get_presure

    current_conditions_display.update(temp, humidity, pressure)
    statistics_display.update(temp, humidity, pressure)
    forecasts_display.update(temp, humidity, pressure)
  end
end

情報を取得して、更新するっていうシンプルなコードです。(これだけじゃもちろん動かないです)

問題の発生

問題点は、~displayという具体的なオブジェクトを直接ロジックに含んでしまっているため、他のdisplayを追加したい時にプログラムの変更をしないといけません。

相互にやり取りするオブジェクトは疎結合にする

構成

f:id:fiveislands:20160814225919j:plain

実際のコード(displayは拡張可能なようにDisplayElementを継承するようになってます)

module Subject
  def register_observer observer
    raise 'should implement register_observer method'
  end
  def remove_observer observer
    raise 'should implement remove_observer method'
  end
  def notify_observers
    raise 'should implement notify_observers method'
  end
end

class WeatherData

  include Subject
  attr_accessor :observers, :temperature, :humidity, :pressure

  def initialize
    @observers = Array.new
  end
  def register_observer observer
    @observers.push(observer)
  end
  def remove_observer observer
  end
  def notify_observers
    @observers.each do |observer|
      observer.update(@temperature, @humidity, @pressure)
    end
  end
  def mesurements_changed
    notify_observers
  end
  def set_mesurements temperature, humidity, pressure
    @temperature = temperature
    @humidity = humidity
    @pressure = pressure
    mesurements_changed
  end
end

module Observer
  def update temp, humidity, pressure
    raise 'should implement update method'
  end
end

class DisplayElement
  include Observer

  def display
    raise 'should implement display method'
  end
end

class CurrentConditionsDisplay < DisplayElement

  attr_accessor :temperature, :humidity, :weather_data

  def initialize weather_data
    @weather_data = weather_data
    weather_data.register_observer(self)
  end
  def update temperature, humidity, pressure
    @temperature = temperature
    @humidity = humidity
    display()
  end
  def display
    puts "Current Weather info: temperature #{@temperature} humidity #{@humidity}"
  end
end

実行コードは

weather_data = WeatherData.new
current_display1 = CurrentConditionsDisplay.new(weather_data)
current_display2 = CurrentConditionsDisplay.new(weather_data)
weather_data.set_mesurements(27, 65, 30.4)

weather_dataのset_mesurementsが呼び出されるとdisplayが更新されるようになってます。
observerを読み込んでいる+コンストラクタでwheather_dataを取り込んでいればweather_dataの情報変更を知ることができるようになります。
このままだと変更をすべて送信してしまうので、subjectのデータを送るかどうかのboolを持つケースもあったりします。(というかそっちの方が多いです)

ボタンをおした時のイベントを取得したりとか、かなり身近なところで使われているパターンということです。

Observerパターンの特徴

Subjectを実装したオブジェクトの状態変化をオブザーバに通知できる。
オブザーバの追加や削除も可能。

参考文献

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

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