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:
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 endIf 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) endThis 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 endAnd 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 endIt'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