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.
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:
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 :)
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