What is an Observer Pattern?
Design patterns are abstract concepts to guide your development not force you into specific implementation. Modern day programs that employ some sort of Observer pattern are more of a nod to the pattern rather than a strict implementation.
The observer pattern is a useful pattern for subscribing to a state in order to be alert of state changes. This is an incredibly common design pattern that you probably use everyday at a high level.
In Javascript the Observer Pattern is used to subscribe event listeners. Java also uses this pattern for listening for clicks in their gui framework.
How does it work?
The Observer pattern will have a state that is being watched for changes by it’s subscribed objects. When the changes occur the subscribed objects will receive an update of the state change and handle it accordingly. In order to drive home this idea we will use the concept of a Bank Account and will be written in Ruby.
Interfaces
An interface in Java is a way of sharing behavior to class objects. When defining these methods they do not return any value and have no method bodies. In fact, when you try to compile your program it will result in an error unless you have defined the method yourself in the class that implements the interface.
public interface Observer {
void update(String balance, String minimum_balance);
}
public interface Notifier {
void send_notification(String balance, String minimum_balance);
}
class SMSNotifier implements Observer, Notifier {
public void update(String balance, String minimum_balance) {
/* Add method body */
}
public void send_notification(String balance, String minimum_balance) {
/* Add method body */
}
}
In Ruby we can do something similar by using modules as mixins and include a
call to raise
inside of the defined methods.
module ContractualBehavior
def promise_youll_implement_me
raise NotImplementedError, "But you promised!"
end
end
class Contractee
include ContractualBehavior
end
[4] pry(main)> Contractee.new.promise_youll_implement_me
NotImplementedError: But you promised!
from (pry):3:in `promise_youll_implement_me'
[5] pry(main)>
You should expect to see an error NotImplementedError: But you promised!
.
We use the concept of an interface in Ruby by defining modules that will raise
an error whenever the mixed in behavior gets invoked. You’ll notice the huge
difference between an interface in java and a module in Ruby is that in Ruby
it’s not required to define the methods without a body.
The value in using an interface pattern with Ruby is the flexibility of behavior. It doesn’t care what you put in the message body. Only that you have defined the mixed in method.
Example in Ruby
Great, now we can start coding, let’s start with creating our program environment.
We need to create a project folder called bank_account
and inside of that
create bin
, lib
and config
folders. Finish off with a bundle init
and edit
the Gemfile
for an important gem called require_all
.
♥ mkdir bank_account && cd bank_account && \
> mkdir bin lib config && \
> bundle init && \
> vim Gemfile
We use the require_all
gem to auto require all files inside of lib
# Gemfile
source "https://rubygems.org"
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem "require_all"
Let’s create some files, one will be config/environment.rb
which is used to
require all the files from lib in one file. Then we need a namespace for our
code which is named after the project, lib/bank_account.rb
♥ bundle install
♥ touch config/environment.rb lib/bank_account.rb && \
> vim config/environment.rb
Inside of our environment file we will require bundler and use it to require
all default gems. That’s any gem not explicitly inside of a group. Our gemfile
has only one Gem at the moment which is getting required and that is require_all
.
We then use require_rel
which is given to us from that gem to require all files
in the lib folder.
# config/environment.rb
require "bundler"
Bundler.require(:default)
require_rel "../lib"
Let’s now add some code to our bank_account
file. We are going to define
a class method of run in order to run our sample code. In order to ensure we
set things up correctly lets output something to system out with puts.
# lib/bank_account.rb
class BankAccount
def self.run
puts "Hey there"
end
end
It’s a good idea to keep to standard programming conventions so we will be using a bin folder with a file named after our project.
touch bin/bank_account.rb
Inside this file we just want to require our environment file and run our
BankAccount
class.
# bin/bank_account.rb
require_relative "../config/environment.rb"
BankAccount.run
Let’s make sure everything works ok.
♥ ruby bin/bank_account.rb
Hey there
Woot! It works!
Observable State
We need a state that is going to be observed. For our project that is the bank account. We want to observe the account for any kind of changes and if it hits a minimal amount that should be there the user will be alerted by SMS and Email.
We already have a BankAccount
class but it needs some behavior that will make
it Observable
. Let’s create a module that will encapsulate methods for us
to ensure our BankAccount
class is observable. We can call this the interface
pattern.
♥ touch lib/observable.rb && vim lib/observable.rb
# lib/observable.rb
module Observable
def add_observer
raise NotImplementedError, "You must implement the instance method add_observer"
end
def remove_observer
raise NotImplementedError, "You must implement the instance method remove_observer"
end
def notify_observer
raise NotImplementedError, "You must implement the instance method notify_observer"
end
end
Now lets include this behavior in our BankAccount
class. What’s happening here
is we are not giving it specific implementation but enforcing a method to be
implemented which in turn will ensure that BankAccount
is observable. We do
this by raising a NotImplementedError
.
Lets include our Observable
interface to the BankAccount
class.
class BankAccount
include Observable
def self.run
puts "Hey there"
end
end
Now let us play with this code a bit. This will require a gem called pry
which
you can install with gem install pry
. This is a debugging tool for ruby that
has some useful helpers to aid in your development.
♥ pry
[1] pry(main)> require_relative './config/environment.rb'
=> true
[2] pry(main)> ls BankAccount
Observable#methods: add_observer notify_observer remove_observer
BankAccount.methods: run
[3] pry(main)>
Awesome, you can see that we now have an interface to make it Observable. When
you use ls BankAccount
we are listing all the instance variables, instance
methods and class methods that are encapsulated by BankAccount
. Now what
happens when we try to call one of these methods?
[2] pry(main)> require_relative './config/environment.rb'
=> true
[3] pry(main)> BankAccount.new.add_observer
NotImplementedError: You must implement the instance method add_observer
from /Users/yianna/Development/code/design_patterns/bank_account/lib/observable.rb:3:in `add_observer'
[4] pry(main)> exit
We now get an error that this behavior needs to be implemented. Another way to enforce behavior is by writing tests which we will go over at the end of the post.
Defining behavior from the flowchart into our BankAccount class.
In order to get rid of these errors we will define the three core methods that are necessary for this pattern to work.
class BankAccount
attr_accessor :balance
attr_writer :changed
attr_reader :minimum_balance
include Observable
def initialize(balance)
@balance = balance
@observers = []
@minimum_balance = 1000
@changed = false
end
def add_observer(observer)
@observers.push(observer)
end
def remove_observer
@observers.delete(observer)
end
def notify_observers
@observers.each { |o| o.update(balance, minimum_balance) }
self.changed = false
end
...
end
Great, now that we have our essential methods in place we can implement the rest
of our BankAccount
class. Lets define minimum_balance?
and inside of the
withdraw
method lets check if we have enough money in the account to do the
withdrawl. If we do have enough then we check for the minimum balance and that
the account indeed did change. If we hit the minimum balance and it has changed
then we will notify the observers.
class BankAccount
...
def minimum_balance?
balance < minimum_balance
end
def withdraw(amount)
unless amount > balance
self.balance -= amount
self.changed = true
end
if minimum_balance? && changed?
notify_observers
end
end
def changed?
@changed
end
def self.run
bank_account = BankAccount.new(1200)
SMSNotifier.new(bank_account)
EmailNotifier.new(bank_account)
bank_account.withdraw(300)
bank_account.withdraw(300)
end
end
Now if we run our code we should get an unitialized constant error. Let’s use this error to guide the next step of our development and define our observers.
♥ ruby bin/bank_account.rb
uninitialized constant BankAccount::SMSNotifier (NameError)
Defining interfaces for our observers
Let’s define two interfaces, one is the observer interface and the other is the notifier interface. Both are enforcing specific behavior that will help us update the observer and send notifications.
♥ touch lib/observer.rb lib/notifier.rb
# lib/observer.rb
module Observer
def update
raise NotImplementedError, "You must implement the instance method update"
end
end
# lib/notifier.rb
module Notifier
def send_notification(balance)
raise NotImplementedError, "Hey define a send_notification instance method."
end
end
Adding our observers to the BankAccount class
In order to subscribe the osbervers to our observable class of BankAccount we
can use an instsantiated bank account as an argument for SMSNotifier and
EmailNotifier. When initializing with a bank account, we will call the
add_observer
method onto the back account while passing self
as an argument;
self
is referring to the currently instantiated class, in our case it will
either be SMSNotifier or EmailNotifier.
class SMSNotifier
def initialize(bank_account)
bank_account.add_observer(self)
end
end
class EmailNotifier
def initialize(bank_account)
bank_account.add_observer(self)
end
end
Defining the behaviors for our observers
Let’s include our Observer and Notifier interfaces into the observer classes.
This will require us to override the update
and send_notification
methods.
# lib/sms_notifier.rb
class SMSNotifier
include Observer
include Notifier
def initialize(bank_account)
bank_account.add_observer(self)
end
def update(balance, minimum_balance)
send_notification(balance, minimum_balance)
end
def send_notification(balance, minimum_balance)
puts %Q(
Hey, your current balance is #{balance} which is below your
minimum_balance of #{minimum_balance}.
)
end
end
Note: %Q
preserves whitespace and allows string interpolation.
# lib/email_notifier.rb
class EmailNotifier
include Observer
include Notifier
def initialize(bank_account)
bank_account.add_observer(self)
end
def update(balance, minimum_balance)
send_notification(balance, minimum_balance)
end
def send_notification(balance, minimum_balance)
puts %Q(
Hey, your current balance is #{balance} which is below your
minimum_balance of #{minimum_balance}.
)
end
end
Running the code
All that’s left is to run our code. You should get the same output as below.
♥ ruby bin/bank_account.rb
Hey, your current balance is 900 which is below your
minimum_balance of 1000
Hey, your current balance is 900 which is below your
minimum_balance of 1000
Hey, your current balance is 600 which is below your
minimum_balance of 1000
Hey, your current balance is 600 which is below your
minimum_balance of 1000
Setting up RSpec
Let’s set up some tests
# Gemfile
gem "rspec"
♥ bundle install
♥ rspec --init
# spec/spec_helper.rb
require_relative '../config/environment.rb'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
end
Adding tests with RSpec
If you would like to skip implementing interfaces you can write tests that will
check to see if those methods have indeed been defined. In the following example
we use RSpec’s respond_to
method to test that it can respond to any method
specified as a symbol.
# spec/bank_account_spec.rb
require_relative './spec_helper'
RSpec.describe BankAccount do
let(:bank_account) { BankAccount.new(1000) }
it "Will respond to an instance method of add_observer" do
expect(bank_account).to respond_to(:add_observer)
end
it "Will respond to an instance method of remove_observer" do
expect(bank_account).to respond_to(:remove_observer)
end
it "Will respond to an instance method of notify_observers" do
expect(bank_account).to respond_to(:notify_observers)
end
end
♥ rspec
...
Finished in 0.00749 seconds (files took 0.26276 seconds to load)
3 examples, 0 failures
Awesome, all of our tests pass. Just to recap; The observer pattern is best when you’re solving a problem that needs to happen in response to a change in state.
Thanks for reading!