今回は4章のおさらいとして、observer(observer.rb)で遊ぼうと思います。
observerってのは、Rubyに標準で同梱されているライブラリです。これはRubyでオブジェクト指向プログラミングのデザインパターンのひとつであるObserverパターンの概念を、簡単に実装できるようにしてくれるもののようです。このソースのコメントに書かれているexampleのスクリプトをいじってみることにします。exampleは以下の通りです。
require "observer"
class Ticker ### Periodically fetch a stock price.
include Observabledef initialize(symbol)
@symbol = symbol
enddef run
lastPrice = nil
loop do
price = Price.fetch(@symbol)
print "Current price: #{price}\n"
if price != lastPrice
changed # notify observers
lastPrice = price
notify_observers(Time.now, price)
end
sleep 1
end
end
endclass Price ### A mock class to fetch a stock price (60 - 140).
def Price.fetch(symbol)
60 + rand(80)
end
endclass Warner ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
@limit = limit
ticker.add_observer(self)
end
endclass WarnLow < Warner
def update(time, price) # callback for observer
if price < @limit
print "--- #{time.to_s}: Price below #@limit: #{price}\n"
end
end
endclass WarnHigh < Warner
def update(time, price) # callback for observer
if price > @limit
print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
end
end
endticker = Ticker.new("MSFT")
WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
ticker.run
簡単に解説します。
Tickerがイベント発生機です。ランダムに金額をfetchし、それをObserverに通知します。通知するためのメソッドがObservableモジュールに定義されていますので、それをincludeしておきます。
Tickerが発生したイベントは、各Observerに通知されます。通知されたイベントに反応するかどうかの判断は各Observer任せです。
WarnHighやWarnLowなどのクラスは、インスタンス生成時(initialize時)に、自分自身をObserverとしてTickerに登録します。
実行してみましょう。
Current price: 83
Current price: 75
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
・・・
こんな感じに延々と出力されるはずです。
はい、ではこれにdRubyを使ってみます。ObservableとObserverのプロセスを別にして、プロセス間通信してみましょう。まずは何も考えず安直に作ってみます。わかりにくいので、各プロセスに、それぞれ便宜的に「Observable側」と「Observer側」という名前をつけることにしますね。
require "observer"
require 'drb/drb'class Ticker ### Periodically fetch a stock price.
include Observabledef initialize(symbol)
@symbol = symbol
enddef run
lastPrice = nil
loop do
price = Price.fetch(@symbol)
print "Current price: #{price}\n"
if price != lastPrice
changed # notify observers
lastPrice = price
notify_observers(Time.now, price)
end
sleep 1
end
end
endclass Price ### A mock class to fetch a stock price (60 - 140).
def Price.fetch(symbol)
60 + rand(80)
end
endclass Warner ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
@limit = limit
ticker.add_observer(self)
end
endclass WarnLow < Warner
def update(time, price) # callback for observer
if price < @limit
print "--- #{time.to_s}: Price below #@limit: #{price}\n"
end
end
endclass WarnHigh < Warner
def update(time, price) # callback for observer
if price > @limit
print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
end
end
endticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run
require 'drb/drb'
class Warner ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
@limit = limit
ticker.add_observer(self)
end
endclass WarnLow < Warner
def update(time, price) # callback for observer
if price < @limit
print "--- #{time.to_s}: Price below #@limit: #{price}\n"
end
end
endclass WarnHigh < Warner
def update(time, price) # callback for observer
if price > @limit
print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
end
end
endticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
スクリプトのダウンロード
[Observable側] 1-1.rb
[Observer側] 1-2.rb
とりあえずできたかな。ではこれで動かしてみます。まずObservable側を動かしてから、次はObserver側を動かします。無事動きました!Observable側のコンソールにObserver側から登録したオブジェクトの出力メッセージが出ています。うまくいきました。
・・・でも、よく考えると微妙です。まず、クラスの定義がどちらのプロセスにも重複して書いてあります。これを別のファイルにしてrequireしたとしても、Observable側が、扱うクラスをすべて知っておかなければならないって言うのは(Javaなどの型縛り言語なら当然だとしても)ちょっとRubyやdRubyの考え方からは外れるような気がします。あと、当然ですがObserver側はすぐ終了しちゃいますね。。で、実行結果のメッセージはすべてObservable側に出ている。で、今までに勉強したとおりdRubyは基本的に値渡しだから、WarnLowやWarnHighは、Observer側のインスタンスじゃなくて、それを複製したオブジェクトをObservable側が持ってしまってるって事です。これはまずいですね。Observer側のプロセスが持っているオブジェクトの内容を参照してもらわないと色々な場面で不便そうです。
そこで、
の、この2点を解決してみたいと思います。じゃあ、まず、単純にObservable側から、Warnerクラスとその派生クラスたちの定義を消して動かしてみます。Observer側は変えませんよ。
require "observer"
require 'drb/drb'class Ticker ### Periodically fetch a stock price.
include Observabledef initialize(symbol)
@symbol = symbol
enddef run
lastPrice = nil
loop do
price = Price.fetch(@symbol)
print "Current price: #{price}\n"
if price != lastPrice
changed # notify observers
lastPrice = price
notify_observers(Time.now, price)
end
sleep 1
end
end
endclass Price ### A mock class to fetch a stock price (60 - 140).
def Price.fetch(symbol)
60 + rand(80)
end
endticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run
スクリプトのダウンロード
[Observable側] 2-1.rb
では、動かします。・・・っと、あれれ?
(druby://[ホスト]:9191) ・・・observer.rb:126 :in `add_observer': observer needs to respond to `update' (NoMethodError)
エラーになっちゃいました。observer.rbのエラーになっている箇所を見てみます。
module Observable
#
# Add +observer+ as an observer on this object. +observer+ will now receive
# notifications.
#
def add_observer(observer)
@observer_peers = [] unless defined? @observer_peers
unless observer.respond_to? :update
raise NoMethodError, "observer needs to respond to `update'"
end
@observer_peers.push observer
end
うーん、respond_to?で聞いた結果、updateってメソッドがないって言われています。なぜでしょう??仕方ないので、ここで、observerの中身を見てみます。ブレークポイントを置いたりしてみましょう。
<DRb::DRbUnknown:0x29a3e5c @buf="\004\bo:\fWarnLow\006:\v@limitiU", @name="WarnLow">
あらら、DRbUnknownってオブジェクトになっていますね。これは前回のエントリで学んだやつです。そうなのか、なるほど、そうですね、考えてみれば当然ですね。WarnLowなどのクラスをObservable側のプロセスは知りません。自分の知らないものが値渡しとして渡されてくると、Marshal.load時に失敗するので駄目ってことですね。なるほど、結局参照渡しにしないと駄目ってことか。では、Observer側を参照渡しに変えてみます。
require 'drb/drb'
DRb.start_service
class Warner ### An abstract observer of Ticker objects.
include DRbUndumped
def initialize(ticker, limit)
@limit = limit
ticker.add_observer(self)
end
endclass WarnLow < Warner
def update(time, price) # callback for observer
if price < @limit
print "--- #{time.to_s}: Price below #@limit: #{price}\n"
end
end
endclass WarnHigh < Warner
def update(time, price) # callback for observer
if price > @limit
print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
end
end
endticker = DRbObject.new_with_uri('druby://[ホスト]:9191')
low = WarnLow.new(ticker, 80)
WarnHigh.new(ticker, 120)
DRb.thread.join
スクリプトのダウンロード
[Observer側] 2-2.rb
3行変えました。
- 参照渡しなので、DRb.start_serviceする
- 参照渡しなので、DRbUndumpedをincludeする
- 実体がこっちのプロセスにあるため、死ぬとまずいのでDRb.thread.joinで待機
これで実行しましょう・・・うまくいきました!メッセージもちゃんとObserver側に出ていますね!めでたしめでたし。なんとなく参照渡しと値渡しの感覚がつかめたような気がします。皆さんもRubyのライブラリをdRubyにしてみたりして遊んでみて下さい。結構簡単に出来ちゃうことに驚くと思います。
これで4章は終了です。