The Observable module is a useful and elegant way to watch for changes in an object in Ruby. A common example is for alerting of some condition within the object being observed. The example shown in the documentation is for a Ticker
class which has two observers: one for when the price goes below a certain level and one for when the price goes above a certain level. What we will do today is show something similar, but for a CoffeeShop
class instead. Let’s get to it!
So the application that we will build will be for a coffee shop where the manager has requested that they be notified when the shop is either full or empty. We will use the Observer pattern for this task!
To get this working we need to do three things:
Observable
module into the CoffeeShop
class.changed
when a customer enters or departs the shop.notify_observers
, passing any usful paramaters, whenever a customer enters or exits the shop.The basic class starts with including Observable and creating an initializer method that takes the name of our shop and the capacity - that is the maximum number of customers the shop can accomodate.
require 'observer'
class CoffeeShop
include Observable
def initialize(name, capacity)
@name = name
@capacity = capacity
@customers = []
end
end
Now we need to create methods that allow a customer to enter and/or depart the shop. Notice that both methods call changed
, then adjust the customers array accordingly and finally calls notify_observers
on the Observable module.
def enter(customer)
changed
@customers.push(customer)
notify_observers(Time.now)
end
def depart(customer)
changed
@customers.delete(customer)
notify_observers(Time.now)
end
Here is our first Observer. Note we pass in the instance of the class that we want the observer to observe (in this case our coffee shop instance). Then we call add_observer
passing in self
(that is the Observer class itself) - which essentially registers the observer with the object we are interested in.
Next note the update
method uses the instance of the coffee shop (@shop) and asks if it’s empty by calling a method we have not defined yet: empty?
. So then if the shop is indeed empty an appropriate alert will be sent to the manager.
class CoffeeShopEmptyObserver
def initialize(shop)
@shop = shop
@shop.add_observer(self)
end
def update(time)
if @shop.empty?
puts "#{time}: The shop is EMPTY so some staff can go home."
end
end
end
Since its clear that we need a method on CoffeeShop to determine if it’s empty we can also define a method for if it’s full as well (so that we can use that in our next observer). Here are the method definitions for empty?
and full?
on the CoffeeShop
class.
def full?
@customers.count >= @capacity
end
def empty?
@customers.count.zero?
end
Next up we need to create the CoffeeShopFullObserver
class so that we can notify the manager to hire more staff! Here is how that looks:
class CoffeeShopFullObserver
def initialize(shop)
@shop = shop
@shop.add_observer(self)
end
def update(time)
if @shop.full?
puts "#{time}: The shop is FULL - quick call more staff!"
end
end
end
Note how the constructor is exactly the same as the constructor in the CoffeeShopEmptyObserver
class. We can refactor this by creating an abstract CoffeeShopObserver class like so:
class CoffeeShopObserver
def initialize(shop)
@shop = shop
@shop.add_observer(self)
end
end
Then we can inherit our concrete observers (for empty or full states) with the new abstract CoffeeShopObserver
. See the link at the end of this post for the complete source code.
So lets get this application up and running and open up our coffee shop and let the customers in (and out!).
First we need to create an instance of our CoffeeShop
class and pass that onto both observers like so:
coffee_shop = CoffeeShop.new("Costa", 3)
CoffeeShopEmptyObserver.new(coffee_shop)
CoffeeShopFullObserver.new(coffee_shop)
For basic usage we can manually assign some customers to enter or depart the shop like so:
coffee_shop.enter("Darren")
coffee_shop.enter("Brian")
coffee_shop.enter("Tammy") #will trigger the alert from the 'full' observer (capacity is set to 3)
coffee_shop.depart("Darren")
coffee_shop.depart("Brian")
coffee_shop.depart("Tammy") #will trigger the alert from the 'empty' observer
The observer pattern as implemented in the Ruby Standard Library is pretty powerful and yet very easy to use. One can imagine using this in a real world application where instead of puts
one might send out an sms alert or an email to a real manager.
Here is a Gist containing the full source code with some extra stuff for randomly generating customers to come and go as they please thus producing a typical ebb and flow of activity in our cool coffee shop!
Rotati is a full service Web and Mobile application consultancy. We use Ruby and JavaScript as our tools of choice.