EventMachine really stole the show when it came to the scene and to my knowledge just became the de facto for anything event based. When Node came around the scene seemed to change to “if you want to do use event loops use JavaScript”.
But those are far from the only places that events are useful in Ruby.
One of my favorite lessons taken from Elixir was the solid emphasis on sending messages. In Elixir, it’s a requirement that you send messages to talk between processes. It’s the only way for one process to talk to another. By requiring processes to communicate through messages, it repeatedly hammers the idea of clearly defined interfaces between processes into the forefront of the developers mind.
Elixir does all this because Erlang’s lightweight processes and functional nature make processes the way to maintain state and organize your application.
The fact is though that all of the best lessons from organizing code using processes and messages are also really awesome when applied to Object-Oriented code in Ruby. Thinking of “calling functions” as “sending messages” is an often touted idea among the best OO devs, so this isn’t anything new. There’s a reason for it. Building code by designing interfaces brings the same benefits to any OO paradigm that Elixir takes advantage of because of the rules forced onto it by Erlang.
In Objective-C, Cocoa makes heavy use of defined interfaces and delegate objects. Throughout building a Cocoa app you will create several objects and set a delegate
property that will be used to process callbacks and customize behavior or the object.
Relying on this requires having a well defined set of public interfaces that delegate
s can implement and an equal set of notifications that are sent by objects.
This paradigm shifts the focus from “calling” functions that manipulate state to sending and receiving messages that react to events in the system.
Shifting this focus places a greater emphasis on building well documented, flexible APIs. Inside my own code, if I can make a system that has a defined interface to maintain whatever state in necessary, I’ve won.
Living this world allows you to leverage events that are already happening in your system. Currently Rails makes use of this in a handful of ways. For instance, the different gems that Rails provides use ActiveSupport.on_load
to take care of initial setup tasks like adding configuration options that each one provides. When you require "active_record"
, it adds an on_load
event listener that gets triggered when some of the base classes get loaded. This makes it possible to both defer configuration and take advantage of eager-loading.
Rails also uses as separate implementation of this in its fantastic instrumentation setup. Rails uses its instrumenters internally to generate most of the awesome log statements you get in development. All of those awesome “Rendered User#index” with timestamps for how long was spend in controllers/views/sql all use this sort of internal pub/sub. In ActiveSupport::Notification
there are examples of how to hook into the default events as well as how to publish your own. It’s worth a read.
Taking advantage of this in my own code means that I’ve had to put enough thought into it that I’ve picked out the events that I need and provided some appropriate interfaces for interrogating the data that I have.
Since I can’t really explain these things without example code, here’s a project where I took advantage of this.
I was writing some code that needed to check the temperature at an interval as well as respond to bluetooth button presses and POSTs over HTTP. I also wanted to provide access to the state of the system so that I could check and make sure it’s running correctly. I also needed to keep track of the state of an external system (a window unit A/C).
There’s a gem called EventBus
by Kevin Rutherford that allows you to register objects to receive events and provides an interface to publish events to them.
To do this I ended up with three events:
-
tick
: for triggering the periodic temperature check. -
toggle
: for triggering a change of state. -
state_changed
: for tracking the change of state. These pass ato
argument.
The objects I ended up with were:
-
StateManager
, which subscribed tostate_change
events to maintain a record of state (on/off). -
TemperatureManager
, which responds totick
. If atick
is received, it checks the temperature and will publish atoggle
message the temperature is too high and the A/C needs to be turned on. - A Sinatra app that publishes a page. If a
POST /toggle
is received it publishes atoggle
message. - an
AirConditioner
object that responds totoggle
messages and turns on and off the A/C. If it’s successful in changing it, it publishes astate_changed
message thatStateManager
will get.
In the end, we have a handful of events and objects all with clear responsibilities and interfaces. Each object is then free to handle those events as they come, or not depending on how that object decides.
Having those events defined also made it easy to add functionality. I went out of town and wanted to have it send me push notifications when my A/C was turned on. I already had a defined place to add that. I simply added another object, a StateChangeNotification
that subscribed and responded to a state_changed
message. Without changing any of the code that actually made the app function I was able to add a whole new feature.
The most obvious critique is that this could also be done with just OO, and that’s 100% valid. But by setting these constraints I force myself to thoroughly explore & design the objects that I end up creating. And in a language that can really encourage just throwing state around places.
So, this is a somewhat off the wall example. Most of the time I’m not writing code to control my air conditioner.
Most growing Rails applications make use of Rails’ ActiveJob
to defer processing of taxing tasks. ActiveJob
, and the older Resque
or Sidekiq
or whatever, are basically a defined way of sending messages without being explicit about sending messages. To an large extent, it’s a bit of an unhelpful abstraction, because for the large part the way I’ve seen it used forces a paradigm that could be useful internally in a process to only be used for inter-process jobs.
I don’t really have an end for this. I could keep rambling on about how amazing message passing is, but this is already entirely too long and needs to stop.