This post will take a look at a few different approaches for decoupling your business logic from Rails. To avoid repeating ourselves, the code below will be assumed to be a bit of Rails controller code, though I should point out that nothing here is Rails specific. When we have a piece of business logic, its calling code may be a rake task, controller, model, an API controller written using Grape/Sinatra, or etc. The point here is to look at how we can avoid duplication and gain flexibility for our callers while keeping our business logic neatly tucked away in a framework-independent place.
First Approach: Classic Rails - Model code with return value
I’m going to skip over the fat controller approach because nobody does that any more. Let’s jump in to the ‘fat model’ approach, which was what people were doing a few years ago when their controllers were getting hairy:
If you’re developing a Rails app of any size, you know that trying to fit too many concerns into models is a nightmare. You end up with bloated classes that do way too many things in nonlinear ways (i.e. reading through the Order class you can’t tell when any particular method might be called). So, let’s start by extracting our checkout process into a separate use case class using the command pattern:
Second Approach: Use Case extraction with return value
Pretty close to textbook Rails, except we’ve taken out the business logic into a command pattern by reifying the checkout process and creating a CheckoutOrder object. Once we get the result, we figure out whether it was a success (or often in Rails land, by checking whether some model is valid), and then act on it by rendering something, sending some emails, etc. This approach has the disadvantage of having to create a special object to hold the return values, and every caller has to deal with the conditional logic for acting on the return values. If I wanted to reuse my CheckoutOrder process from a Grape API, for example, I’d have to reproduce the conditional there.
Third Approach: Naive callbacks
To avoid conditional logic in the callers, we can move the conditional inside the business object and tell us when it succeeds or fails via callbacks. The naive approach is passing “self” to the command:
This approach improves on the first by eliminating the conditional. The downside to this is that you’ve now polluted your caller (typically a Controller in Rails land) with nonlinear methods. By this, I mean that the calling code is not inside the controller, and thus it’s difficult to know when or how they may be called. Additionally, these methods are interspersed with other methods that are user-facing controller actions. Not pretty.
Four Approach: The decorated self-shunt
We can improve the intermixing of callback methods with actual controller methods by encapsulating the responder logic:
This seems cleaner just from a readability perspective, as it makes it clear those methods are not part of the controller’s regular flow, but actually just callbacks.
Fifth Approach: A listener framework
At this point, we arrive at the conclusion that we want to use callbacks to avoid creating special response objects, but we want to maintain a somewhat linear readability to the code. Luckily, there’s a very lightweight pub/sub library called Wisperthat provides exactly that. Let’s take a look at a controller action refactored to wisper:
You’ll note I also threw in an AnalyticsListener to show you how Wisper supports both block subscribers (great for controllers) and object subscribers (great for orthogonal concerns like analytics). The benefit of this model is it’s very easy to add additional concerns - logging, analytics, notifications by simply adding subscribers, without modifying your core business logic. Additionally Wisper supports the notion of global listeners that can be engaged for all Wisper publishers.
At Reverb, our AnalyticsListener is global so that all we have to do in order to listen to a new analytics event is to add another method to the listener: Listeners are Plain Old Ruby Objects and publishers in the wisper land are simply an object that does an “include Wisper::Publisher”.
Although the resulting controller code may appear heavier than the plain old Rails approach, you’ll soon find that this lets you build a very modular system with a clean approach for orthogonal concerns, as well as reuse of business logic across many entry points such as API, Rails, Rake, Console, and etc.
We’ve been experimenting with Wisper for a few months now at Reverb, and believe it has really cleaned up our code and helped separate things into areas of single (or at least diminished) responsibility. Share your thoughts! We’d love to hear about other people dealing with large Rails/Ruby projects and how you’re dealing with growing your software in a modular way.