Rails - the Missing Parts - Interactors
Here at Grouper, we’re long-time users of Ruby on Rails – along with other New York startups like RapGenius and Kickstarter. It’s simple to pick up and is optimized for developer productivity.
However, people have started noticing the downsides – once codebases evolve past a few thousand lines of code, test suites often become sluggish and the framework load-time increases.
We’ve come to realise it doesn’t have to be this way – a number of unhelpful Rails features encourage poor design patterns, which in turn lead to tightly-coupled code and slow, unmaintainable test-suites.
While Rails may be the perfect tool for building an MVP, it isn’t optimised for medium and large codebases. We’re solving these problems with 3 concepts we believe should be part of any “Advanced” Rails deployment; Interactors, Policies and Presenters.
Part 1 – Interactors
The overwhelming trend in Rails codebases is for the majority of business logic to reside in very large God classes in the ActiveRecord
/models directory. This is normally a clue that each class has too many responsibilities, a vast public API and methods that require the presence of a complex graph of associated objects in order to function at all.
The nail in the coffin is ActiveRecord callbacks;
before_save hooks on one class that modify the state of other objects. This is a problem because those other objects are now required to be present before we can play with the object we care about. When testing, we are stuck between tediously slow test-suites (as these objects are often fetched from the database) or the laborious process of stubbing out callbacks and long chains of method calls.
The solution is a concept called “Interactors” (often called “Service Objects”. Interactors demand that the core of your application should live in a set of plain-old-Ruby-objects (POROs) that are responsible for the main use-cases of your application, leaving your ActiveRecord classes as skinny interfaces to your data-store. By looking at the names of your Interactors, you should be able to tell what your application does;
AssignBarForGrouper, etc. Classes like
Bar just validate and store attributes like name, location and date.
(By way of background – Grouper organises “Groupers” – 3-on-3 drinks events at bars between groups of friends. They’re a lot of fun)
There’s a very lightweight gem called Interactor which exposes a couple of convenient methods like
failure?, so your controllers can now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
and your interactor might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
The advantages are numerous – both in terms of code-complexity and test-speed. For one, your tests should now be lightning-fast because you rarely need to hit the database, and you can test each ActiveRecord model in isolation without worrying about its associations. The Interactors themselves can be be tested with doubles rather than ActiveRecord models, and Rails often doesn’t even need to be loaded by your test suite.
Your controller spec might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
With all ActiveRecord database calls stubbed out, this test file now runs in around 0.15 seconds, compared to around 4 or 5 seconds previously.
Additionally, your application will be easier to reason about because each class has a single responsibility – the
Bar class knows where the bar is located and when it’s open, for example. It doesn’t need to know about
Groupers. The complex logic for determining which
Bar to assign to which
Grouper can sit safely on its own in the
Finally, if you keep your Interactors small and single-purpose, you can compose multiple Interactors together for more complex operations. This mix-and-match approach further decouples your codebase and lets you reuse operations as necessary.
You obviously need to choose the patterns that fit the problem you’re trying to solve – it’s rarely one-size-fits-all, and some of these principles may be overkill in a very simple 15-minute blog application. Our objection is that Rails feels like it’s a Beginners’ Version, often suggesting that one size does fit all. Once your codebase surpasses a very basic level of complexity, you’re left on your own grasping for solutions. We’d suggest Interactors are one of these solutions.
For the next in the series, see Policy Objects – slimming down your controllers.
If you’re interested in Rails design-patterns and best-practices, you should check out our jobs page – we’re hiring.
You can also get involved in the discussion on hacker news.