« 4章.参照渡しと値渡し (その7) | メイン

4章.参照渡しと値渡し (その8)

今回は4章のおさらいとして、observer(observer.rb)で遊ぼうと思います。
observerってのは、Rubyに標準で同梱されているライブラリです。これはRubyでオブジェクト指向プログラミングのデザインパターンのひとつであるObserverパターンの概念を、簡単に実装できるようにしてくれるもののようです。このソースのコメントに書かれているexampleのスクリプトをいじってみることにします。exampleは以下の通りです。

require "observer"

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def 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
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = 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側」という名前をつけることにしますね。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def 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
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

class Warner     ### An abstract observer of Ticker objects.
 def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = Ticker.new("MSFT")
DRb.start_service('druby://[ホスト]:9191', ticker)
ticker.run

[Observer側]

require 'drb/drb'

class Warner     ### An abstract observer of Ticker objects.
def initialize(ticker, limit)
  @limit = limit
  ticker.add_observer(self)
 end
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = 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側のプロセスが持っているオブジェクトの内容を参照してもらわないと色々な場面で不便そうです。
そこで、


  • Observable側が、扱うクラスを知ってないと駄目なのはだるそう

  • Observable側にObserverのインスタンスが値渡しされているのは不便そう

の、この2点を解決してみたいと思います。じゃあ、まず、単純にObservable側から、Warnerクラスとその派生クラスたちの定義を消して動かしてみます。Observer側は変えませんよ。

[Observable側]

require "observer"
require 'drb/drb'

class Ticker     ### Periodically fetch a stock price.
 include Observable

 def initialize(symbol)
  @symbol = symbol
 end

 def 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
end

class Price      ### A mock class to fetch a stock price (60 - 140).
 def Price.fetch(symbol)
  60 + rand(80)
 end
end

ticker = 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側を参照渡しに変えてみます。

[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
end

class WarnLow < Warner
 def update(time, price)    # callback for observer
  if price < @limit
   print "--- #{time.to_s}: Price below #@limit: #{price}\n"
  end
 end
end

class WarnHigh < Warner
 def update(time, price)    # callback for observer
  if price > @limit
   print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
  end
 end
end

ticker = 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章は終了です。

トラックバック

このエントリーのトラックバックURL:
http://www.grandnature.net/bin/mt-tb.cgi/72

コメントを投稿

About

2007年11月14日 10:45に投稿されたエントリーのページです。

ひとつ前の投稿は「4章.参照渡しと値渡し (その7)」です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。