The goal of this article is to show the differences in thinking that arise when you develop your application design using different languages and language paradigms. We will have a dynamic object-oriented language (Ruby), static object-oriented language (C#) and functional actor model-based dynamic language (Elixir). I may speculate that this might be interesting to people who know one or two of these languages.
Observer can be a really useful pattern when you need to notify other parts of your system about a state change or an event. The pattern is also known as publish/subscribe (pub/sub for short). Almost every GUI framework uses this pattern to receive notifications about button click or some other interaction with GUI. It is less common in server-side apps, but none the less, I’ve seen it be used in certain scenarios where it is a great fit.
Please, note that all examples will be canonical and by no means perfect or suitable for production use.
Also, I would like to mention that I will not implement the unsubscribing part here, because it will make my examples longer and won’t contribute to the understanding of the idea.
With that in mind, let’s begin!
Observer pattern in Ruby
In Ruby, there is an Observable module in the standard library to achieve publish/subscribe constructs, but we will write our own module to demonstrate what is happening under the hood.
Let’s start by building our own ObservableImplementation
module that will be included in our observable class:
module ObservableImplementation def observers @observers || @observers = [] end def notify_observers(*args) observers.each do |observer| observer.update(*args) end end def add_observer(object) observers << object end end
As you can see, observers
the method is just a simple array that is getting initialized at the first call. The notify_observers
method is the one that object will call whenever you want to notify listeners about the state change in it. This method in itself is relatively simple, it will just go through the collection of observers and call .update
on each of them. It would be up to the user of this module to determine the interface of .notify_observers
and .update
. Of course, we need a way to add observers to our object and that’s what add_observer
is meant for. It will simply add an object to the observer’s collection.
Now, when we have a ObservableImplementation
module, let’s create a class that will notify subscribers about the state change of our counter.
class Observable include ObservableImplementation attr_accessor :counter def initialize self.counter = 0 end def tick self.counter += 1 notify_observers(counter) end end
As you can see, we include our ObservableImplementation
module at the top to add observer functionality. We also set the value of our counter to 0
upon initialization. In method, .tick
it will call .notify_observers
the method that will be using functionality from ObservableImplementation
module. In essence, every time method .tick
has been called, we notify our subscribers about the state change.
Now, the only thing that we need to do, is to add listeners. I will create a class that will listen to the state change and react upon that.
class Observer def initialize(observable) observable.add_observer(self) end def update(counter) puts "Count has increased by: #{counter}" end end
Upon initialization, we pass an observable object and we are adding subscriber to it as a reference self
. Another thing that we need to take care of, is the .update
method that is required to implement in order to receive notification from the publisher.
Now, let’s launch our code in irb:
irb(main):005:0> observable = Observable.new irb(main):006:0> observer = Observer.new(observable) irb(main):007:0> observer = Observer.new(observable) irb(main):008:0> observable.tick Count has increased by: 1 Count has increased by: 1 irb(main):009:0> observable.tick Count has increased by: 2 Count has increased by: 2
We instantiate an observable object, instantiate two observers, and call .tick
on observable. You can see that our observers receive notifications and everything works fine here.
Now let’s move to the observer implementation in C#.
Observer pattern in C#
In C# world we need to think about our code a little bit differently than in Ruby. If we have different instances of a different class of listener, we need to define observers interface. Let’s implement our abstract Observable class first and carry on with everything else as we go.
abstract class Observable { private List<IObserver> observers = new List<IObserver>(); public void AddObserver(IObserver observer) { observers.Add(observer); } public void Notify() { observers.ForEach(o => o.Update()); } }
We’ve created this class to have the behaviour that would be similar to what we’ve had in Ruby. This is an abstract class so it will need to have the implementation. We will have private property observers
of type List
that will contain IObserver
type objects, or in other words, objects that have implemented IObserver
interface. Let’s go ahead and implement it.
interface IObserver { void Update(); }
It is an interface that has to implement a single method Update
. Now, this code should be able to compile. Next thing we need to do is to implement ConcreteObservable
class that will be able to leverage our abstract Observable
class.
class ConcreteObservable : Observable { public int SubjectState {get; set; } public ConcreteObservable() { this.SubjectState = 0; } public void AddToCounter() { this.SubjectState ++; } }
We will have a SujectState
property and in our constructor, we set it to 0
. Also, we’ve added a state modifier method AddToCounter()
. Its only job is to increase our SubjectState
property by 1
. Of course, it inherits from our Observable
class.
So the next thing we need to think about is the concrete observer itself. Let’s define it.
class ConcreteObserver : IObserver { private ConcreteObservable Observable; public ConcreteObserver(ConcreteObservable observable) { this.Observable = observable; } public void Update() { Console.WriteLine("Counter has increased: {0}", Observable.SubjectState); } }
This class will take the observable object in the constructor. In order to access its state, it will have Update()
method, that will print our state, or in our case counter, from the Observable
. And we are done. the only thing left to do is to bring it all together:
class MainClass { static void Main() { ConcreteObservable observable = new ConcreteObservable(); observable.AddObserver(new ConcreteObserver(observable)); observable.AddObserver(new ConcreteObserver(observable)); observable.AddToCounter(); observable.Notify(); observable.AddToCounter(); observable.Notify(); observable.AddToCounter(); observable.Notify(); } }
We are instantiating new ConcreteObservable instance and then adding two observers to it. Also, please, note that we are passing observable itself as a reference to the observer. This is needed in order to access state of observable. Then we call our AddToCounter()
method and notify our observers three times. And now let’s compile and launch our code by typing mcs observer.cs && mono observer.exe
and we should get the following output:
Counter has increased: 1 Counter has increased: 1 Counter has increased: 2 Counter has increased: 2 Counter has increased: 3 Counter has increased: 3
In case of C#, there are consequences of static typing. It’s not as easy to do as it is in Ruby, but none the less, it’s doable and it should not bother you that much if you are a seasoned C# developer. There are also several ways we can do “sort of duck typing” in C# with Generics. But this is not a topic of this blog post.
Now it is time to move to the sweet spot of this post – Elixir.
Observer pattern in Elixir
Elixir is where things get interesting. Let’s start by creating our observable process.
defmodule Observable do def start do spawn(__MODULE__, :listen, [[], 0]) end def listen(subscribers, count) do receive do {:add_subscriber, subscriber_pid} -> add_subscriber(subscribers, subscriber_pid) |> listen(count) {:add_to_counter} -> new_count = count + 1 notify_subscribers(subscribers, new_count) listen(subscribers, new_count) end end def add_subscriber(subscribers, subscriber_pid) do [subscriber_pid | subscribers] end def notify_subscribers(subscribers, count) do Enum.each(subscribers, fn(subscriber_pid)-> send(subscriber_pid, count) end) end end
Let’s discuss what we have written here – First of all, we’ve defined the module Observable
. It has a function start
that spawns a process (Elixir process, not OS process) and calls listen/2
the function on Observable
module (__MODULE__
corresponds to the name of the module we are in) with an empty list. Why do we pass [[], 0]
as a second argument? We do that because that is the way you can pass multiple arguments to function dynamically. Imagine that we needed to pass two symbols as two arguments to our listen function. It that case we would do it like this: [:first_arg, :second_arg]
. Okay, we’ve spawned a process, now what? listen/2
the function is now executed and has arguments of the empty list as subscribers
and count
as 0
in separate isolated process and receive do
part is listening for the incoming messages. Right now it can receive two messages – {:add_subscriber, subscriber_pid}
and {:add_to_counter}
. Elixir helps us to distinguish messages by pattern matching against them.
So, when we receive a message with tuple {:add_susbscriber, subscriber_pid}
we add a new subscriber to the existing list and call listen/2
again with the new state. At this point, if you are not familiar with recursions, your brain should start to hurt. This functionality is achieved via tail call. There is another interesting operator |>
. It is similar to pipe operator in *NIX systems. a(b())
essentially means the same as b |> a
. With that in mind, we call a function add_subscriber/2
and this function appends pid (Process identifier – don’t worry, we will talk about it bit later) to the list of the existing pids and the result of that function is passed to the recursive listen/2
function as the first argument. The count
has not changed in this case, so we just pass what we’ve had.
Let’s pick apart the second case when we receive a {:add_to_counter}
message. As soon as we get this message, we call notify_receivers/2
function which goes through the list of our subscribed pids and sends them a message with the new incremented count. Then we just call our recursive listen/2
function with the new count and pid list of our subscribers.
Okay, next thing that we are going to do is adding subscribers (observers) to our main process.
defmodule Observer do def start do spawn(__MODULE__, :listen, []) end def listen do receive do event -> IO.puts("Received #{event}") end listen end end
So, when we start this process it will listen for the incoming events and print them on the screen. Again, after receiving a message, it will just call itself and listen for another message until the world ends. Let’s try to launch this in our console and see what happens.
iex(1)> observable = Observable.start #PID<0.67.0> iex(2)> send(observable, {:add_subscriber, Observer.start}) {:add_subscriber, #PID<0.69.0>} iex(3)> send(observable, {:add_subscriber, Observer.start}) {:add_subscriber, #PID<0.71.0>} iex(4)> send(observable, {:add_to_counter}) Counter has increased: 1 Counter has increased: 1 iex(5)> send(observable, {:add_to_counter}) Counter has increased: 2 Counter has increased: 2
So, as you can see, we’ve started the observable process and it has returned us a pid. So what is a pid?. Pid is a process identifier in the BEAM virtual machine and the way you interact with erlang/elixir processes is by sending them messages. So observable
value is holding pid of our observable process that we launched. Next thing to do is to send the observable {:add_subscriber, Observer.start}
message. We are starting another Observer process and the function start/0
returns us a pid of our Observer process. So, we’ve spawned a process and sent its pid to our Observable process. We’ve done that four times. Afterwards, we just send {:add_to_count}
to our observable process and it notifies its four subscribers that the count has increased. There are few consequences to what we’ve done here: all these launched processes work in parallel, they are isolated and that means that we can use our two/four/eight cores without even thinking too much about it.
Conclusion
You might say that this comparison is not really fair, but in sense it actually is. Mostly, we are dealing with natural constructs of languages and the way you think about your application is expressed in those primitives. In case of .NET there is a library AKKA.NET that will allow building actor based systems on CLR and Mono environments so I would guess that the industry is starting to shift towards more concurrent systems and mechanisms that can achieve this concurrency. Also, Matz (Creator of Ruby) has tweeted that he wants to build actors into the Ruby and disable GIL, which is great. Also for Ruby ecosystem, there is Celluloid.
If I have missed something or some part needs more explanation, I would appreciate, if you could leave a comment.