1# 2# Implementation of the _Observer_ object-oriented design pattern. The 3# following documentation is copied, with modifications, from "Programming 4# Ruby", by Hunt and Thomas; http://www.ruby-doc.org/docs/ProgrammingRuby/html/lib_patterns.html. 5# 6# See Observable for more info. 7 8# The Observer pattern (also known as publish/subscribe) provides a simple 9# mechanism for one object to inform a set of interested third-party objects 10# when its state changes. 11# 12# == Mechanism 13# 14# The notifying class mixes in the +Observable+ 15# module, which provides the methods for managing the associated observer 16# objects. 17# 18# The observers must implement a method called +update+ to receive 19# notifications. 20# 21# The observable object must: 22# * assert that it has +#changed+ 23# * call +#notify_observers+ 24# 25# === Example 26# 27# The following example demonstrates this nicely. A +Ticker+, when run, 28# continually receives the stock +Price+ for its <tt>@symbol</tt>. A +Warner+ 29# is a general observer of the price, and two warners are demonstrated, a 30# +WarnLow+ and a +WarnHigh+, which print a warning if the price is below or 31# above their set limits, respectively. 32# 33# The +update+ callback allows the warners to run without being explicitly 34# called. The system is set up with the +Ticker+ and several observers, and the 35# observers do their duty without the top-level code having to interfere. 36# 37# Note that the contract between publisher and subscriber (observable and 38# observer) is not declared or enforced. The +Ticker+ publishes a time and a 39# price, and the warners receive that. But if you don't ensure that your 40# contracts are correct, nothing else can warn you. 41# 42# require "observer" 43# 44# class Ticker ### Periodically fetch a stock price. 45# include Observable 46# 47# def initialize(symbol) 48# @symbol = symbol 49# end 50# 51# def run 52# lastPrice = nil 53# loop do 54# price = Price.fetch(@symbol) 55# print "Current price: #{price}\n" 56# if price != lastPrice 57# changed # notify observers 58# lastPrice = price 59# notify_observers(Time.now, price) 60# end 61# sleep 1 62# end 63# end 64# end 65# 66# class Price ### A mock class to fetch a stock price (60 - 140). 67# def Price.fetch(symbol) 68# 60 + rand(80) 69# end 70# end 71# 72# class Warner ### An abstract observer of Ticker objects. 73# def initialize(ticker, limit) 74# @limit = limit 75# ticker.add_observer(self) 76# end 77# end 78# 79# class WarnLow < Warner 80# def update(time, price) # callback for observer 81# if price < @limit 82# print "--- #{time.to_s}: Price below #@limit: #{price}\n" 83# end 84# end 85# end 86# 87# class WarnHigh < Warner 88# def update(time, price) # callback for observer 89# if price > @limit 90# print "+++ #{time.to_s}: Price above #@limit: #{price}\n" 91# end 92# end 93# end 94# 95# ticker = Ticker.new("MSFT") 96# WarnLow.new(ticker, 80) 97# WarnHigh.new(ticker, 120) 98# ticker.run 99# 100# Produces: 101# 102# Current price: 83 103# Current price: 75 104# --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75 105# Current price: 90 106# Current price: 134 107# +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134 108# Current price: 134 109# Current price: 112 110# Current price: 79 111# --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79 112module Observable 113 114 # 115 # Add +observer+ as an observer on this object. so that it will receive 116 # notifications. 117 # 118 # +observer+:: the object that will be notified of changes. 119 # +func+:: Symbol naming the method that will be called when this Observable 120 # has changes. 121 # 122 # This method must return true for +observer.respond_to?+ and will 123 # receive <tt>*arg</tt> when #notify_observers is called, where 124 # <tt>*arg</tt> is the value passed to #notify_observers by this 125 # Observable 126 def add_observer(observer, func=:update) 127 @observer_peers = {} unless defined? @observer_peers 128 unless observer.respond_to? func 129 raise NoMethodError, "observer does not respond to `#{func.to_s}'" 130 end 131 @observer_peers[observer] = func 132 end 133 134 # 135 # Remove +observer+ as an observer on this object so that it will no longer 136 # receive notifications. 137 # 138 # +observer+:: An observer of this Observable 139 def delete_observer(observer) 140 @observer_peers.delete observer if defined? @observer_peers 141 end 142 143 # 144 # Remove all observers associated with this object. 145 # 146 def delete_observers 147 @observer_peers.clear if defined? @observer_peers 148 end 149 150 # 151 # Return the number of observers associated with this object. 152 # 153 def count_observers 154 if defined? @observer_peers 155 @observer_peers.size 156 else 157 0 158 end 159 end 160 161 # 162 # Set the changed state of this object. Notifications will be sent only if 163 # the changed +state+ is +true+. 164 # 165 # +state+:: Boolean indicating the changed state of this Observable. 166 # 167 def changed(state=true) 168 @observer_state = state 169 end 170 171 # 172 # Returns true if this object's state has been changed since the last 173 # #notify_observers call. 174 # 175 def changed? 176 if defined? @observer_state and @observer_state 177 true 178 else 179 false 180 end 181 end 182 183 # 184 # Notify observers of a change in state *if* this object's changed state is 185 # +true+. 186 # 187 # This will invoke the method named in #add_observer, passing <tt>*arg</tt>. 188 # The changed state is then set to +false+. 189 # 190 # <tt>*arg</tt>:: Any arguments to pass to the observers. 191 def notify_observers(*arg) 192 if defined? @observer_state and @observer_state 193 if defined? @observer_peers 194 @observer_peers.each do |k, v| 195 k.send v, *arg 196 end 197 end 198 @observer_state = false 199 end 200 end 201 202end 203