Managing Side-effects With the Pub-Sub Model
Over time, large-scale object-oriented systems tend to produce God Objects. These are classes which know too much or do too much. They have connections to disparate and varied parts of the system. They depend on everything, and everything depends on them. They make systems slow to work with, intractable and hard to modify. They insidiously undermine and resist our efforts to carry out our core task as engineers: decomposing complex problems into smaller subproblems that are more easily solved.
At Causes there are some concepts that are front-and-center in our product: users, their campaigns and the actions they create to make an impact (things like petitions, fundraisers and pledges). The concepts are so core that they have a tendency to become God Objects unless we diligently work to prevent them accruing more and more functionality.
A case study in managing side-effects: taking action
When a user takes action on our site by, say, signing a petition, there is potentially a slew of side-effects:
- we persist a record of the signature to a
signaturestable in the database, and an
ActionCredit(effectively a hundreds-of-millions-of-rows journal of all action-taking activity on our site)
- counter-caches tick up
- stats events are generated and dispatched to one or more tracking systems
- a recruiter may receive an on-site notification or an email
- invitations may be marked as accepted (in the case of the recruiter) or “indirectly accepted” (in the case of other, multiple inviters)
- Facebook Request objects may be cleared out
- feed events are generated and propagated to feeds
- a custom Open Graph action is published
- if the action pushed the campaign over a milestone, a milestone event may be generated, which itself could result in feed events being propagated, onsite notifications, and an email to the campaign organizer
- if the action is sponsored by a brand, the signature could trigger a donation to a nonprofit (which itself would have other side-effects)
And this is only scratching the surface. Having the
Action class know about
all these collaborators effectively makes it depend on them just as much as they
depend on it, and it places the class squarely within “God Object” territory.
PubSubHub we have a centralized registry of events, together with the
listeners that wish to be informed of those events. Our
Action class now just
has to make a call to
PubSubHub.trigger to let its collaborators know that
something important happened:
1 2 3 4 5 6
This greatly reduces the clutter and makes the separation between core mechanics and secondary side-effects clear.
Listener registration looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
We considered making listener registration more distributed, via a DSL that could be sprinkled into other classes, but in the end having the central registry provides us with a nice inventory of the relationships and dependencies between different parts of the system. It also conveniently avoids load-order issues in the context of a large Rails app, whose loading behavior (eager vs lazy, cached vs uncached) is different between development and production; we can just stick the registration in an initializer and be done with it.
Note that with this change we’ve inverted the dependencies such that the
Action class no longer depends on a bunch of other classes;
rather, those other classes depend on it. From the perspective of the
class, this is a good thing: if your goal is to build something that is both
robust and useful, depending on as little as possible and having others depend
on you is a good thing.
One other nicety of this system is that it gives us a straight-forward way to divide side-effects into the urgent and the non-urgent, the latter being run asynchronously.
The final piece of the puzzle are the various listeners. By convention, they
implement a handler of the form
1 2 3 4 5 6 7 8
This slender little library has allowed us to scoop out a lot of functionality
Action class, making it significantly less god-like. New engineers
are able to arrive in the implementation file for the first time and comprehend
the core structure and functionality more rapidly, free from the distraction of
a bunch of secondary and tertiary side-effects.
If you’d like to see what PubSubHub can do for your code base, it’s only a
install pubsubhub away, and the source
code is up on GitHub.
We’re mindful that to the person with a hammer, everything looks like a nail, so we’re careful to ensure that we use the tool judiciously. In the context of a Rails application, this means that we continue to make use of Rails’ built-in tools for managing side effects (things like Active Record life-cycle callbacks, observers, and Active Support Notifications).
Additionally, our eyes are ever on the prize, asking the question, “How can we make this simpler?” The Pub-Sub pattern is a tool for loosening the coupling between parts of the system, but it does not entirely eliminate that coupling. Complexity is the ultimate enemy, and the best way to manage side-effects is to simply eliminate them in the first place.