Saturday, October 13, 2012

Observer Pattern In Ruby

Let add one more pattern into the design pattern series by consider Observer pattern. In observer pattern, there is a subject which will be observed by multiple observers. When an event happen with the subject, all observers who registered with the subject will get notify. One obvious example of this pattern is you and my blog. If you are subscribed to my blog, you will get notify when my blogs get updated. This relation between subject and observers provide a clear pattern. It's clear enough to be implemented as a module in ruby standard library, called Observable.

In this post, I am going to use this observable module to implement a program that allow me follow stocks prices, and also calculate the average over time.

Assume that the stock market allow me to read current stock price with a function call "get_current_price(stock_symbol)".  With this method, I can implement a simple class, SimpleStockWatcher, to read the current price and calculate the average as follow:
class SimpleStockWatcher 
  def initialize(symbol)
    @symbol = symbol
    @sum = 0
    @count = 0
  end

  def update
    price = get_current_price(@symbol)
    @sum += price
    @count += 1
    show(price)
  end

  def show(price)
    puts "Symbol #{@symbol}"
    puts "  Time:          #{Time.now}"
    puts "  Current Price: #{price}"
    puts "  Average:       #{@sum/@count}"
  end
end
If I want to monitor Bank of America (BOA), and Google (GOOG), I implement the following loop:
stocks = [ SimpleStockWatcher("BOA"), SimpleStockWatcher("GOOG") ]
loop do
  stocks.each(&:update)
  sleep(1)
end
This loop will print the stocks price and average very second.

Now, I want to add more analysis, say 30 or 100 days moving average. No problem, I add more code into the SimpleStockWatcher. Then every second we get the stock prices, average, 30-day moving average, and 100-days moving average.

Let get a bit more complicate, I want to show only current price and 30-days moving average for stock "BOA". For "GOOG", I want to show current price, the average, and 100-days moving average. How do I satisfy this requirement?


Look carefully, StockWatcher is doing two things. One is get current price for symbols. For any stock, StockWatcher does this in the same way. Another part of StockWatcher is doing different analysis on the price. This part is different from stock to stock. So the analysis are parts of StockWatcher need to be able to change. If you follow my previous posts, you know we need to separate the analysis parts from StockWatcher. In this case, I separate the analysis parts into CurrentPriceAnalyzer, AverageAnalyzer, MovingAverageAnalyzer, etc. Each analyzer is responsible for only one analysis.

However, there is a specific relation between the StockWatcher and these analyzer objects. Basically, when the stock price change, the StockWatcher will notify any analyzers. Here, where the observer pattern come in. The pattern is already implemented as a module within ruby standard library, called Obervable, and I am going to use it to implement a solution to this problem.

Now, StockWatcher has only responsibility to get the stock price and notify the analyzers if the price change. Most of the implementation is taking care of by including the Observable Module:
require "observer"

class StockWatcher
  include Observable

  def initialize(symbol)
    @symbol = symbol
    @price = 0.0
  end 

  def update
    read_current_price
    notify_observers(Time.now, @symbol, @price)
  end 

  def read_current_price
    next_price = get_current_price(@symbol)
    if @price != next_price
      changed 
      @price = next_price
    end 
  end 
end

Observable Module provides methods changed and notify_observers which used in the StockWatcher. The changed method need to be called when the price change. This will set an internal stage and allow the notify_observers to send notification to registered observers.

The notify_observers pass it arguments to the observers when the notification happen. The observers have to implement a method that receive the same list of argument. For example, the CurrentPrinceAnalyzer implement update method to receive the notification from StockWatcher, as shown:
class CurrentPriceAnalyzer
  def update(time, symbol, price)
    puts "#{time} #{symbol}: price #{price}"
  end
end
And that all it need for CurrentPriceAnalyzer. It's only show the current price.

On the other hard, the AveragePriceAnalyzer need to do a little bit more:
class AveragePriceAnalyzer
  def initialize
    @sum = 0.0 
    @count = 0 
  end 

  def average
    @sum/@count
  end 

  def show(time, symbol)
    puts "#{time} #{symbol}: average #{average}"
  end 

  def update(time, symbol, price)
    @sum += price
    @count += 1
    show(time, symbol)
  end 
end
It's keep the sum and count how many time it's get notify, and calculate average from these internal stage.

Finally I can implement the loop to get update stock price, and do analysis, as follow:
boa = StockWatcher.new("BOA")
goog = StockWatcher.new("GOOG")

current = CurrentPriceAnalyzer.new
boa_avg = AveragePriceAnalyzer.new
goog_avg = AveragePriceAnalyzer.new
thirty = MovingAverageAnalyzer.new(30)
hundred = MovingAverageAnalyzer.new(100)

boa.add_observer(current, :update)
boa.add_observer(boa_avg, :update)
boa.add_observer(thirty, :update)
goog.add_observer(current, :update)
goog.add_observer(goog_avg, :update)
goog.add_observer(hundred, :update)

loop do
  boa.update
  goog.update
  sleep(1)
end

The method add_observer, which also provided by Observable Module, is called to add an observers to the subject, a StockWatcher object. It's also tell the subject which method to call when the observers need to be notify.

In this case, the BOA stock watcher object has 3 analyzers, current, boa_avg, and thirty. All implement update method for notification. Similarly, Google stock watcher also has 3 analyzers, current, goog_avg and hundred. Notice that the boa and goog can not use the same instance for AveragePriceAnalyzer, because it's has internal stage specific for each stock.

Again, it's come down to the word change. We still have to identify what need to be changed and separate that part out of the object. However, with the specific relation between subject and observers, we can easily identify what part need to be separated out (and become observers). The relation also provide a clear pattern, and it is clear enough to be generalized and implemented into a module. All we need to do are, include and know how to use them.

No comments:

Post a Comment