Objectify: A Better Way to Build Rails Applications
For almost 6 years, the dominant "best practice" for building rails applications has been skinny controller, fat model. In other words, put all of your business logic in to your models — keeping it out of your controllers. The result is typically a small number of bloated objects that are impossible to reason about or test in isolation [1].
That property is important. To understand why, let's take a quick and highly selective look at the origins of object oriented programming.
On Encapsulation
One of the early papers that emphasized the importance of encapsulation in software development was James H. Morris Jr.'s Protection in Programming Languages. He argued that if we were going to be able to write correct software, “programs” (probably most analogous to objects in the OOP world) needed “protection” from each other.
...hostility is not a necessary precondition for catastrophic interference between programs. An undebugged program, coexisting with other programs, might as well be regarded as having been written by a malicious enemy—even if all the programs have the same author!
We offer the following as a desideratum for a programming system: A programmer should be able to prove that his programs have various properties and do not malfunction, solely on the basis of what he can see from his private balliwick.
The idea is that if we can prove that components work in isolation, then we have a better chance of having a functioning system when we assemble them in to a larger whole.
Aside from being central to the thesis in a paper that heavily influenced the development of object oriented programming itself, it doesn't seem like a stretch to argue that components that are provably correct in isolation would make a good building block from which to build working systems [2].
We can derive a lot of good object oriented practices from this idea. Two that are relevant here:
-
Single responsibility principle (SRP): To be able to prove that an object works correctly in isolation, that object's behaviour has to be clearly defined. The more responsibilities an object has, the more complex its behaviour becomes, and is therefore more difficult to prove and reason about.
For example, if we put all of our business logic in ActiveRecord::Base subclasses, the burden of proving that our code is correct becomes immense because we expose ourselves to mistaken interactions with all of the behaviour we've inherited. ActiveRecord::Base already has a responsibility: persistence.
- Dependency injection (DI): Many objects have dependencies — other objects that they collaborate with to accomplish their goals. If those dependencies aren't configurable in some way, then we can't test the behaviour of an object without implicitly testing its collaborators as well. By injecting our dependencies, we can easily replace them with alternative implementations. In our tests, we can inject fake objects in order to isolate the object in question.
In Search of Something Better
So, how should we organize our applications? There are a few approaches floating around.
One in particular is something I've been thinking about and refining for a while now [3]. In this approach, persistence objects remain extremely thin, and business logic is encapsulated in lots of very simple objects known as “services” and “policies”. Not all objects in this methodology will fit in to one of those two categories, but they are two of the most important concepts.
Service objects are responsible for retrieving and/or manipulating data — essentially any work that needs to be done that you might have put in a controller or model object before. They typically only have one method, which I like to define as “#call” (they're usually named something like PictureCreationService, so naming the method #create would seem redundant).
Policy objects are responsible for enforcing access control policies. We use them in before filters to guard controller actions, reuse them in views to conditionally display pieces of UI (example: a delete button that requires administrative privileges), and anywhere else policies need enforcing, like background jobs. Policy objects only ever have one public method: “#allowed?”.
Composability
Decomposing the behaviour of our rails application makes it extremely simple for us to prove that our objects work in isolation because we're adhering to SRP. As a bonus, since all of our unit tests inject test doubles — that don't actually do any real work — in place of real collaborators, our tests are extremely fast.
There's one other major benefit to this approach: that which has been decomposed can also be recomposed. In other words, because our behaviours are factored in to many focused objects instead of a small number of bloated ones, we can (and do) recompose them to create other things. If you set out to build a User object, all you'll ever have is a User object; if you build the pieces of a User object, then you get a User object and any number of other things you can build from its pieces.
Objectify
We've been following and refining this approach with a real app of nontrivial complexity in production for a few years now, and it's been very successful. However, certain things have always felt somewhat ad hoc, and since rails isn't designed to support this kind of structure, it's easy for people (myself included) to get confused about exactly how functionality should be structured.
So, I spent the last few weeks building a framework that sits on top of rails and exposes these abstractions (and a few others) as first-class citizens. It's called objectify. It's very far from being perfect or finished, but it's a start, and we're already using it to clean up sections of our code with great success. You can read more about objectify in the README.
If you're interested in the future of objectify and better OOP practices for rails apps (and elsewhere), fork the code, join the mailing list, come hang out in #objectify on freenode, or just hit me up on twitter.
P.S. If you're interested in working on this kind of stuff and/or on a rails app that has been committed to these practices literally for years, get in touch because we're hiring!
- [1] Note that the skinny controller, fat model approach doesn't necessarily indicate that business logic belongs in ActiveRecord::Base subclasses, and even mentions presenter objects. The article wasn't necessarily was wrong, but peoples' implementation of it has been.
- [2] Barbara Liskov credits Morris's paper as being one of the primary influences of her Turing Award-winning research on abstract data types (a predecessor to what we now think of as classes).
- [3] Another option is to implement all of our functionality as a series of modules, which we then include in to our model classes (or extend our model objects with). It is possible to test these modules in isolation by creating fake objects to include them on. But a lot of care still needs to be taken to make sure that modules being included together don't interfere with each other, which means that these modules aren't encapsulated. Without encapsulation, the benefits of isolated testing are mostly negated. This approach is actually equivalent to putting everything in the same class, except for the fact that it's spread out over multiple source files.