Observer design pattern in Crystal language
Being a developer you probably have heart about Observer design pattern. Perhaps, you have even used it in your complex system with subscribers and notifications. This article is about how to implement Observer pattern in Crystal language and what are the common features of this language we can use there to tune it. If you are not familiar with Observer design pattern I will suggest you to read this article before.
Say, we are developing a game where two units (fighters) fight with each other. Each fighter has a name and amount of health. Fighter can make a damage to other fighter. If fighter’s health is 0 - fighter is dead. Our another requirement is to update stats when fighter is damaged to let player know current health of his fighter. And the last thing we would like to notice is a notification about death. We want to congratulate a winner or do some other actions.
Of course, first that comes to the mind is a popular Mortal Kombat video game and I will suggest you to do not hesitate to imagine it in that way. Actually, we will not write a new game or something like that, we just need a concept.
Next are going to implement this in high level manner.
Observable (or Subject)
At first we need to develop a Fighter
class. Here is how it might look:
class Fighter
getter name, health
def initialize(@name)
@health = 100
end
def damage(rate)
if @health > rate
@health -= rate
else
@health = 0
end
end
def is_dead?
@health <= 0
end
end
It meets our fighter’s requirements: fighter has a name, health and can be damaged by another fighter.
The idea of an Observer pattern is to notify subscribers when subject’s state changes. Subject in our case is represented by Fighter
class. But it need to be able to notify observers when fighter is damaged. This is where Observable
modules comes (the most interesting part):
module Observable(T)
getter observers
def add_observer(observer)
@observers ||= [] of T
@observers.not_nil! << observer
end
def delete_observer(observer)
@observers.try &.delete(observer)
end
def notify_observers
@observers.try &.each &.update self
end
end
We want to emphasize few points:
This is a module (not a class) because we want to include all this functionality in our
Fighter
class and leave a way to inheritFighter
from another class in future. Crystal does not support multiple inheritance thus we can use the same approach as used by Ruby’s built-in Observer.We used generics (type
T
) to define type of an observer. This makes our subject more general and it is not coupled with concrete class.We initialize a list of observers on demand (another idea from Ruby’s built-in Observer). That’s why our list of observers at some point of time may be
nil
and that’s why we need to usetry
andnot_nil!
methods to ensure that we do not call observer’s methods onnil
object and prevent compile errors.
We can’t include Observable
module in Fighter
class currently because we do not have an Observer
. In other words, we do not know a type of T
. So, let’s create few observers.
Observer
Here is how an interface for our Observer might look:
abstract class Observer
abstract def update(fighter)
end
Then we can implement concrete observers (Stats
and DieAction
):
class Stats < Observer
def update(fighter)
puts "Updating stats: #{fighter.name}'s health is #{fighter.health}"
end
end
class DieAction < Observer
def update(fighter)
puts "#{fighter.name} is dead. Fight is over!" if fighter.is_dead?
end
end
The last thing we need to do is to include Observable
module into our Fighter
class:
class Fighter
include Observable(Observer)
#...
end
Notice how we define a type of our Observable
module when we include it.
Wrapup
We are ready to run a simple example:
# Sample
fighter = Fighter.new("Scorpion")
fighter.add_observer(Stats.new)
fighter.add_observer(DieAction.new)
fighter.damage(10)
# Updating stats: Scorpion's health is 90
fighter.damage(30)
# Updating stats: Scorpion's health is 60
fighter.damage(75)
# Updating stats: Scorpion's health is 0
# Scorpion is dead. Fight is over!
Crystal’s type system is very flexible. It allows us to use generics, helps to prevent runtime errors and gives ability to write concise and easy to read code. In our implementation of Observer pattern we may found examples of all mentioned points.
Source code for example used in this article you may find in Crystal Patterns Github repo.
Leave a Comment