October 27, 2007 by alex
We recently introduced a new feature in autoki called the social feed. It’s basically a yellow box displaying any events on the platform relevant to the current user, like a friend has posted a new photo, or a new interesting car was uploaded. The data model behind this is pretty straightforward, we have a FeedEvent class and all kinds of subclasses, e.g. a MessageReceivedEvent. Each event belongs to a user and an event source, in this example the user would be the user who received the message and the event source would be the message itself. For each user, we simply display all the events that belong to him or her.
Now the question was this: How do we create these events? The most straightforward way would probably have been to create them in the models, so the Message model would have an after_create callback that created the event. What we didn’t like about this solution was that we would put a whole bunch of logic into the models that didn’t really belong there. Why would a Message care if there was some kind of event feed? Plus these events would be all around in our unit tests and make the bloated and probably sloooow (again). So we wanted to use the observer pattern to remove the creation of the event from the models.
For observers in a Rails app you basically have two choices waiting there for you: the ruby Observable mixin and the ActiveRecord::Observer class. We didn’t have much time (as usual) and only took a very short look at both and quickly decided to go with the ruby Observable. ARObservers seemed to only allow the usual before/after create/update/save/destroy callbacks and looked much more heavyweight than the tiny Observable module. So we did this: (sorry new example, this time with comments.)
Wow. That’s a whole lot of code for a tiny little yellow box with one sentence in it. And it’s not even complete because you have to handle deletion of the comments and some other stuff as well. (no event if user comments on his own objects).
Anyway, for some reason we implemented this for 8 types of events or so: Integration test, attach observer in controller, call
notify_observers method in model, create event in observer. It was a real pain because we were implementing almost the same thing again and again. Especially attaching the observer in the controller seemed too much work. We thought about doing some meta ruby magic to be able to do the same as rails does with cache sweepers. Instead of attaching the sweeper to the model directly you simply declare that you want this cache sweeper to be active in this action:
It didn’t really work out because we couldn’t figure out the right magic to do the right things when the model was loaded or created without letting the model do it, which would make the whole use of observers pointless.
Finally after a couple of days we took a closer look at the ARObservers. They don’t require you to add them to a model instance every time you want to use them. All you have to do in order to use them is to configure them in your environment:
You then derive you observer class from ActiveRecord::Observer and from then on get all your before/after create/update/save/destroy events for free, delivered right to your observer method with the corresponding name:
Now we still had the problem that we wanted custom events, e.g. an after_read event. After some digging deep down in the rails sources we found out how:
That’s it. Now we implement an after_read method in the observer and we were done.
Any idea how the rails guys got the observers attached to every model instance in the app automagically? Well, they attached the observer to the class, not the instance. Sounds like a brilliant idea doesn’t it? But wait. We said one big reason for us to use the observers was that we didn’t want our unit tests to be concerned with this. And now the observers were attached to the classes and all our tests were carrying them as well. Our first solution was to move the observer configuration from environment.rb to the development.rb/production.rb files but this only solved one problem while creating two others: We now had the observer configuration in multiple places and, more importantly, our integration tests didn’t have the observers as well, hence, were now failing.
We ended up doing this: moved the observer configuration back into environment.rb, removed the observers from the model classes before running the unit tests, attached them back before running the integration tests:
(We have a separate directory for our obserers in app/obserers)
For our integration tests we created a new file integration_test_helper.rb which we now require instead of the test_helper.rb:
With this setup we now have working unit and integration tests. No need for controller tests and implementation, model tests and implementation only if we have a custom event (such as after_read). What’s left are integration tests and a unit test and implementation of the observer, which is mostly trivial: