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:
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):
We want to emphasize few points:
This is a module (not a class) because we want to include all this functionality in our
Fighterclass and leave a way to inherit
Fighterfrom 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
niland that’s why we need to use
not_nil!methods to ensure that we do not call observer’s methods on
nilobject 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.
Here is how an interface for our Observer might look:
Then we can implement concrete observers (
The last thing we need to do is to include
Observable module into our
Notice how we define a type of our
Observable module when we include it.
We are ready to run a simple example:
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.