Observer Pattern
深入淺出設計模式 讀書會
第二章 觀察者模式
講者:Eugene
課前準備
git clone https://github.com/yuyueugene84/weatherapp.git
回顧
-
設計守則
- 將變動部份封裝
- 多用合成,少用繼承
- 針對介面寫程式(多型),不要針對實踐寫程式(知道太多細節)
- 策略模式把個別演算法(行為)封裝起來,讓它們可以互換,而不影響到使用
氣象觀測 APP
1. 使用 WeatherData 物件追蹤天氣狀況
2. 三個布告板
需求 | |
---|---|
天氣狀況 | 溫度(C)、濕度(%)、氣壓(kPa) |
氣象統計 | 最低溫度、最高溫度 |
天氣預報 | 文字顯示天氣狀態 |
打開 WeatherData 類別...
class WeatherData
def get_temperature
#取得最新氣溫的方法
end
def get_humidity
#取得最新溼度的方法
end
def get_pressure
#取得最新氣壓的方法
end
def measurement_changed
#一旦氣象資料有更新,此方法會被呼叫
end
end
實作 MeasurementChanged 方法
class WeatherData
def measurement_changed
#呼叫方法取得最新資料
temp = get_temperature
humidity = get_humidity
pressure = get_pressure
#更新所有佈告板
@current_conditions_display.update(temp, humidity, pressure)
@statistics_display.update(temp, humidity, pressure)
@forecast_conditions_display.update(temp, humidity, pressure)
end
end
直覺的寫出方法...
但是...
class WeatherData
def measurement_changed
#呼叫方法取得最新資料
temp = get_temperature
humidity = get_humidity
pressure = get_pressure
#更新所有佈告板
@current_conditions_display.update(temp, humidity, pressure)
@statistics_display.update(temp, humidity, pressure)
@forecast_conditions_display.update(temp, humidity, pressure)
end
end
1. 針對實踐寫程式
2. 若有新的佈告板,就需要修改這裡的程式碼
3. 無法動態增加或刪除布告板
4. 尚未封裝改變的部分
觀察者模式 Observer Pattern
當物件之間有一對多的關係,並且當一個物件的狀態或資料發生變化時,希望相依的所有物件都可自動收到更改的狀態或資料
用白話一點的說法...
就是出版者和訂閱者的關係!

出版者 + 訂閱者 = 觀察者模式

相關名詞中英對照
中文 | English |
---|---|
主題 / 出版者 | Subject / Publisher |
觀察者 / 訂閱者 | Observer / Subscriber |
概念
1. 當 Subject 有內容改變,會通知所有關注該 Subject 的 Observer 並更新
2. Observer 可以訂閱 或 取消訂閱 Subject
3. 取消訂閱就不會收到 Subject 通知
4. 一個 Subject 可被多個 Observer 訂閱
類別圖說明

觀察者模式 類別圖

鬆綁 Loose Coupling
- 盡量讓需要互動的物件鬆綁
- 好處是讓物件間的相依性降到最低 (彼此不用知道對方太多細節)
- 由上圖可得知 觀察者模式 可以讓 Subject 和Observer 之間鬆綁
觀察者模式如何鬆綁
-
Subject 只知道 Observer 有實作特定介面
- 但不知道細節
- 任何時後都能加入/移除 Observer
-
新型態的 Observer 加入,Subject 不用修改程式碼
- Subject 只在乎 Observer 有沒有實作它的介面
-
兩者不必綁在一起使用,可被單獨使用
- 各自實作各自的介面
- 只要介面被遵守,改變任一個都不會影響另一個
觀察者模式 類別圖

觀察者模式 方法整裡
public interface Subject {
public void registerObserver(Observer o); //註冊觀察者
public void removeObserver(Observer o); //移除觀察者
public void notifyObserver(); //若狀態更新,通知所有觀察者
}
public interface Observer {
public void update(float temp, float humidity, float pressure); //被呼叫時,把物件的資料更新,然後呼叫 display 顯示資料
}
public interface DisplayElement {
public void display(); //顯示資料
}
實作 Subject 模組
module Subject
def initialize
@observers = []
end
def register_observer(observer)
@observers << observer
end
def remove_observer(observer)
@observers.delete observer
end
def notify_observers
@observers.each do |observer|
observer.update(self.temp, self.humidity, self.pressure)
end
end
end
實作 WeatherData 類別
class WeatherData
include Subject
attr_accessor :temp, :humidity, :pressure
def measurements_changed
notify_observers
end
def set_measurements(temp, humidity, pressure)
# 更新資料
self.temp = temp
self.humidity = humidity
self.pressure = pressure
measurements_changed() # 通知所有觀察者更新並顯示最新資料
end
end
實作 Observer 相關模組
module Observer
def update(temp, humidity, pressure)
self.temp = temp
self.humidity = humidity
self.pressure = pressure
display
end
end
module DisplayElement
def display
puts "目前情況 : 溫度 = #{temp}度C, 濕度 = #{humidity}%, 壓力 = #{pressure}kPa"
end
end
實作 CurrentConditionDisplay 類別
class CurrentConditionsDisplay
include Observer, DisplayElement
attr_accessor :temp, :humidity, :pressure, :subject
def initialize(weatherData)
self.subject = weatherData
self.subject.registerObserver(self)
end
def update(temp, humidity, pressure)
self.temp = temp
self.humidity = humidity
self.pressure = pressure
display
end
def display
puts "目前情況 : 溫度 = #{temp}度C, 濕度 = #{humidity}%, 壓力 = #{pressure}"
end
end
實作 WeatherStation 類別
class WeatherStation
def initialize
# 主程式,new 出 WeatherData 和 CurrentConditionsDisplay 物件
puts 'WeatherStation Start'
w = WeatherData.new
c = CurrentConditionsDisplay.new(w)
# 丟測試用資料
w.setMeasurements 80, 65, 30.4
w.setMeasurements 82, 70, 29.2
w.setMeasurements 78, 90, 29.2
end
end
用假資料太無聊了...
用 Yahoo API 取得真的氣象資料
select item.forecast, item.condition, atmosphere from weather.forecast where woeid = 2306179 and u="c" limit 1
改寫 WeatherData Class
class WeatherData
include Subject
attr_accessor :temp, :humidity, :pressure
def measurements_changed
notifyObservers
end
def setMeasurements(temp=nil, humidity=nil, pressure=nil)
url = 'https://query.yahooapis.com/v1/public/yql?q=select%20item.forecast%2C%20item.condition%2C%20atmosphere%20%20%20from%20weather.forecast%20where%20woeid%20%3D%202306179%20and%20u%3D%22c%22%20limit%201&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys'
uri = URI(url)
response = Net::HTTP.get(uri)
obj = JSON.parse(response, object_class: OpenStruct)
self.temp = ( temp != nil ) ? temp : obj.query.results.channel.item.condition.temp
self.humidity = ( humidity != nil ) ? humidity : obj.query.results.channel.atmosphere.humidity
self.pressure = ( pressure != nil ) ? pressure : obj.query.results.channel.atmosphere.pressure
measurements_changed()
end
end
Ruby 內建的 Observable 模組
class WeatherData
# 只要加入這一行
include Observable
attr_accessor :temp, :humidity, :pressure
def setMeasurements(temp=nil, humidity=nil, pressure=nil)
url = 'https://query.yahooapis.com/v1/public/yql?q=select%20item.forecast%2C%20item.condition%2C%20atmosphere%20%20%20from%20weather.forecast%20where%20woeid%20%3D%202306179%20and%20u%3D%22c%22%20limit%201&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys'
uri = URI(url)
response = Net::HTTP.get(uri)
obj = JSON.parse(response, object_class: OpenStruct)
changed
self.temp = ( temp != nil ) ? temp : obj.query.results.channel.item.condition.temp
self.humidity = ( humidity != nil ) ? humidity : obj.query.results.channel.atmosphere.humidity
self.pressure = ( pressure != nil ) ? pressure : obj.query.results.channel.atmosphere.pressure
notify_observers(self)
# measurements_changed()
end
end
至於原本的 Subject 模組呢?
# module Subject
# def initialize
# @observers = []
# end
# def register_observer(observer)
# @observers << observer
# end
# def remove_observer(observer)
# @observers.delete observer
# end
# def notify_observers
# @observers.each do |observer|
# observer.update(self.temp, self.humidity, self.pressure)
# end
# end
# end

完全不需要了~
Observable 會自動實作以下方法:
1. add_observer
2. delete_observer
3. notify_observers
改寫 Observer 模組
module Observer
#def update(temp, humidity, pressure)
def update(obj) #改成傳入物件,改動彈性比較大
raise NotImplementedError, 'Implement this method!'
end
end
重構 CurrentConditionDisplay 類別
class CurrentConditionsDisplay
include Observer, DisplayElement
attr_accessor :temp, :humidity, :pressure, :subject
def initialize(weatherData)
self.subject = weatherData
# self.subject.registerObserver(self)
self.subject.add_observer(self) #物件產生時就把自己加入 weatherdata 的觀察者
end
#def update(temp, humidity, pressure)
def update(obj)
self.temp = obj.temp
self.humidity = obj.humidity
self.pressure = obj.pressure
display
end
def display
puts "目前情況 : 溫度 = #{temp}度C, 濕度 = #{humidity}%, 壓力 = #{pressure}kPa"
end
end
還沒結束!
實作 StatisticsDisplay 類別
class StatisticsDisplay
include Observer, DisplayElement
attr_accessor :high_temp, :low_temp, :subject
def initialize(weatherData)
self.subject = weatherData
self.subject.add_observer(self)
end
def update(obj) #到 obj 裡取得自己需要的資料即可,介面不用和其他顯示一樣
self.high_temp = obj.high_temp
self.low_temp = obj.low_temp
display
end
def display
puts "天氣統計 : 最高溫度 = #{high_temp}度C, 最低溫度 = #{low_temp}度C"
end
end
實作 ForecastDisplay 類別
class ForecastDisplay
include Observer, DisplayElement
attr_accessor :forecast, :subject
def initialize(weatherData)
self.subject = weatherData
self.subject.add_observer(self)
end
def update(obj)
self.forecast = obj.forecast
display
end
def display
puts "天氣預報 : #{forecast}"
end
end
重構 WeatherData 類別
class WeatherData
include Observable
attr_accessor :temp, :humidity, :pressure, :high_temp, :low_temp, :forecast
def set_measurements(temp=nil, humidity=nil, pressure=nil, high_temp=nil, low_temp=nil, forecast=nil)
obj = get_yahoo_data # 去 yahoo API 取得資料,存入 obj 變數中
changed # 一個 flag,告訴 Ruby 該物件的狀態/資料已更改,接下來 notify_observer 才會通知所有觀察該物件的觀察者已改變
self.temp = ( temp != nil ) ? temp : obj.query.results.channel.item.condition.temp
self.humidity = ( humidity != nil ) ? humidity : obj.query.results.channel.atmosphere.humidity
self.pressure = ( pressure != nil ) ? pressure : obj.query.results.channel.atmosphere.pressure
self.high_temp = (high_temp != nil) ? high_temp : obj.query.results.channel.item.forecast.high
self.low_temp = (low_temp != nil) ? low_temp : obj.query.results.channel.item.forecast.low
self.forecast = ( forecast != nil ) ? forecast : obj.query.results.channel.item.forecast.text
# 更新所有的狀態,若傳入的參數有值,就用參數的值,若沒有,就用 yahoo 的資料
notify_observers(self)
# measurements_changed()
end
def get_yahoo_data
url = 'https://query.yahooapis.com/v1/public/yql?q=select%20item.forecast%2C%20item.condition%2C%20atmosphere%20%20%20from%20weather.forecast%20where%20woeid%20%3D%202306179%20and%20u%3D%22c%22%20limit%201&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys'
uri = URI(url)
response = Net::HTTP.get(uri)
obj = JSON.parse(response, object_class: OpenStruct)
end
end
大功告成!
class WeatherStation
def initialize
puts 'WeatherStation Start'
w = WeatherData.new
CurrentConditionsDisplay.new(w)
StatisticsDisplay.new(w)
ForecastDisplay.new(w)
w.set_measurements
# now = Time.now
# end_time = now + 30
# 在 30 秒內,不斷丟假資料給 WeatherData,讓它自動顯示新資料
# begin
# w.set_measurements rand(1..40), rand(1..100), rand(10000..20000), rand(20..40), rand(1..20), ['sunny', 'rain'].sample
# now += 1
# end while now < end_time
puts 'WeatherStation Ends'
end
end
實用層面:MVC 架構

Q&A
The End
deck
By Eugene Chang
deck
- 771