Sunday 14 December 2014

DESIGN PATTERN BY EXAMPLE (IN RUBY) - OBSERVER PATTERN

Hi everybody and welcome back.

In this article, I've been talking about how useful can be the Template and the Strategy patterns. We refactored existing code to use these two patterns and the result was a less coupled and more flexible application.

We designed a solution for a simple problem: model a game where an oracle thinks of a number in a certain range and each participant makes a number of guesses to find out what the number is. After each guess, the oracle tells the participant if the are correct or not, and if not, whether their guess was too big to too small. 

We produced the following class diagram:





Now lets add some complexity to the original problem;

Let's say that there is one more entity interested in knowing when a game finishes with a participant either guessing the number correctly or using up all their attempts.


This entity is the Auditor class. The role of the auditor is to observe all participants and ensure that their win/loss profile is in keeping with what would be expected and highlight any anomalies.

This kind of problem is a good example of a problem that can be solved using the Observer design pattern
The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state change, usually by calling one of their methods. It is mainly used to implement distributed event handling systems.

The Observer pattern is also a key part in the familiar model–view–controller (MVC) architectural pattern. The observer pattern is implemented in numerous programming libraries and systems, including almost all GUI toolkits.

We created one module called subject that acts as Observable object.
The participant will include this module so we can avoid the usage of inheritance (this is a specific Ruby construct). 
The Subject module contains all the common methods  to an observable object
  • adding and observer
  • deleting and observer  
  • notify an observer;
 module Subject  
  def initialize  @observers=[]  
  end  
  def add_observer(observer)  
   @observers << observer   
  end  
  def delete_observer(observer)  
   @observers.delete(observer)  
  end  
  def notify_observers    
   @observers.each do |observer|     
   observer.update(self)  
  end   
 end  

Now, every time a participant plays, will also notify its observers as you can see highlighted in red:


 require_relative 'subject.rb'  
 require_relative 'oracle.rb'  
 # Tries to guess the 'secret' number using several different strategies  
 class Participant  
  include Subject  
  attr_accessor :num_attempts  
  attr_reader :playStrategy  
  attr_reader :oracle  
  attr_reader :max_num_attempts  
  attr_reader :name  
  attr_reader :age  
  attr_reader :strategyName  
  def initialize(name,age,oracle,playStrategy, max_num_attempts:10)  
   super()  
   @name=name;  
   @age=age;  
   @strategyName=playStrategy.name;  
   @oracle, @max_num_attempts = oracle, max_num_attempts  
   @num_attempts = 0  
   @playStrategy = playStrategy  
  end  
  def play(lower,upper)  
   result = @playStrategy.play(lower,upper,self)  
   notify_observers()  
   result  
  end  
  def reset  
   @num_attempts = 0  
  end  
 end  


Successively, we create an other class called Auditor. The auditor will be an observer of the class Participant and it will try to understand if the Participant is cheating or not. 
Every time a participant plays, the auditor is notified and it will run some specific business logic for understanding if the participant has cheated. In this case it will print out a string. If the participant is a looser it will not doing anything. We do not want to catch loosing cheaters :)

 class Auditor  
  attr_reader :treshold;  
  def initialize  
   @treshold=3;  
  end  
  def update( changed_participant )  
   if (isAWinner?(changed_participant))  
     analizeWinner(changed_participant)  
   elsif  
     analizeLoser(changed_participant)  
   end  
  end  
  private  
  def isAWinner?( participant )  
   participant.num_attempts <= participant.max_num_attempts  
  end  
  private  
  def analizeLoser (particpant)  
  end  
  private  
  def analizeWinner( participant )  
   if isASuspect?(participant)  
    puts "#{participant.name} is a suspect because he won with only #{participant.num_attempts} attempts playing with #{participant.strategyName}"  
   end  
  end  
  private  
  def isASuspect?(participant)  
   participant.num_attempts < @treshold  
  end  
 end  


Finally, we created a DishonestParticipant class. This Participant is a cheater. He knows the oracle number. We simulated this condition overriding the method play to return always success on the first attempt. This shown that the method play should be secured to be a final method. Nobody should be able to override it because changing its logic, all the rules could be broken.

 require_relative 'participant'  
 class DishonestParticipant < Participant  
  def play(lower,upper)  
   num_attempts=1;  
   notify_observers()  
   :success  
  end  


The main application will have to change to register the auditor observer object with the participant observable object:


 # Evaluate the performance of participants using different guessing strategies  
 require_relative 'auditor.rb'  
 require_relative 'oracle.rb'  
 require_relative 'participant.rb'  
 require_relative 'binary_search_play'  
 require_relative 'linear_play'  
 require_relative 'play_strategy'  
 require_relative 'smart_random_play'  
 require_relative 'random_play'  
 require_relative 'dishonest_participant'   
 NUM_OF_RUNS = 8  
 oracle = Oracle.new  
 # evaluate random strategy  
 total_num_attempts = 0  
 total_num_failures = 0  
 auditor = Auditor.new  
 ageAuditor = AgeAuditor.new  
 homer = Participant.new("Homer",40,oracle, RandomPlay.new, max_num_attempts: NUM_OF_RUNS*2)  
 homer.add_observer(auditor)  
 1.upto(NUM_OF_RUNS) do |i|  
  oracle.secret_number = i  
  homer.reset  
  if homer.play(1,NUM_OF_RUNS)==:success  
   total_num_attempts += homer.num_attempts  
  else  
   total_num_failures += 1  
  end  
 end  
 puts "play randomly took on average #{total_num_attempts/(NUM_OF_RUNS-total_num_failures)} attempts to succeed"  
 # evaluate linear strategy  
 total_num_attempts = 0  
 total_num_failures = 0  
 bart = Participant.new("Bart",18,oracle, LinearPlay.new, max_num_attempts:NUM_OF_RUNS*2)  
 bart.add_observer(auditor)  
 1.upto(NUM_OF_RUNS) do |i|  
  oracle.secret_number = i  
  bart.reset  
  if bart.play(1,0)==:success  
   total_num_attempts += bart.num_attempts  
  else  
   total_num_failures += 1  
  end  
 end  
 puts "play_linear took on average #{total_num_attempts/(NUM_OF_RUNS-total_num_failures)} attempts to succeed"  
 # evaluate 'smart random' strategy  
 total_num_attempts = 0  
 total_num_failures = 0  
 maggie = Participant.new("Maggie",3,oracle, SmartRandomPlay.new,max_num_attempts:NUM_OF_RUNS*5)  
 maggie.add_observer(auditor)    
 1.upto(NUM_OF_RUNS) do |i|  
  oracle.secret_number = i  
  maggie.reset  
  if maggie.play(1,NUM_OF_RUNS)==:success  
   total_num_attempts += maggie.num_attempts  
  else  
   total_num_failures += 1  
  end  
 end  
 puts "play_smart_random took on average #{total_num_attempts/(NUM_OF_RUNS-total_num_failures)} attempts to succeed"  
 # evaluate binary search strategy  
 total_num_attempts = 0  
 total_num_failures = 0  
 lisa = Participant.new("Lisa",15,oracle,LinearPlay.new, max_num_attempts:NUM_OF_RUNS*5)  
 lisa.add_observer(auditor)  
 lisa.add_observer(ageAuditor)  
 1.upto(NUM_OF_RUNS) do |i|  
  oracle.secret_number = i  
  lisa.reset  
  if lisa.play(1,NUM_OF_RUNS)==:success  
   total_num_attempts += lisa.num_attempts  
  else  
   total_num_failures += 1  
  end  
 end  
 puts "play_binary_search took on average #{total_num_attempts/(NUM_OF_RUNS-total_num_failures)} attempts to succeed"  
 # dishonest participant  
 total_num_attempts = 0  
 total_num_failures = 0  
 luca = DishonestParticipant.new("Luca",27,oracle,LinearPlay.new, max_num_attempts:NUM_OF_RUNS*5)  
 luca.add_observer(auditor)  
 1.upto(NUM_OF_RUNS) do |i|  
  oracle.secret_number = i  
  luca.reset  
  if luca.play(1,NUM_OF_RUNS)==:success  
   total_num_attempts += lisa.num_attempts  
  else  
   total_num_failures += 1  
  end  
 end  

Here is a full class diagram for the new application:





Just a quick disclaimer, do not abuse this pattern because it can cause memory leaks, known as the lapsed listener problem, because in basic implementation it requires both explicit registration and explicit deregistration, as in the dispose pattern, because the subject holds strong references to the observers, keeping them alive. This can be prevented by the subject holding weak references to the observers.

Happy coding

Luca

No comments:

Post a Comment