So most people when they think of Evented programming in Ruby, their mind immediately jumps to EventMachine, or HTTP requests, or maybe if they’re really a nerd SAX processing XML documents.
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
delegates 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 a
The objects I ended up with were:
StateManager, which subscribed to
state_changeevents to maintain a record of state (on/off).
TemperatureManager, which responds to
tick. If a
tickis received, it checks the temperature and will publish a
togglemessage the temperature is too high and the A/C needs to be turned on.
- A Sinatra app that publishes a page. If a
POST /toggleis received it publishes a
AirConditionerobject that responds to
togglemessages and turns on and off the A/C. If it’s successful in changing it, it publishes a
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
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.