I’d like to present the marrying of a few techniques that have transformed the way we write Rails apps at UserVoice, and the gem we extracted from it. But first, let me explain why we started looking into techniques that veered off the Omakase version of Rails.
We had three specific problems at UserVoice:
We have different user interfaces to UserVoice: the front-end where folks come to look at ideas and vote, the admin console where admins moderate their ideas and respond to tickets, the API, widgets, a Facebook app, the iOS SDK, etc. All of these interfaces have their own controllers. We have the potential for duplicate logic: there could be an
ideas#createendpoint in multiple controllers.
In the API especially, folks would pass in all kinds of invalid input. For instance, if an
params[:idea][:name], they’d just pass
params[:idea]as the string
"Here's my idea". This would pollute our exception logs.
Some of our models became extremely fat, with a sequence of lifecycle callbacks that feed into each other. It became hard to reason about and modify these models, especially when you’re dealing with logic that has to work in multiple contexts (create vs update vs business action A vs business action B).
We’ve done three things to solve this.
First, as is more and more common in the Prime Stack, we’re been using the Service Layer pattern more. Each service object is essentially a module of code that has one primary method and one responsibility. For instance:
We’ve called these service objects ‘Mutations’, as quite often they represent a change of state in our database from state A to state B.
Second, we took a hard look at input validation and made some big changes. In particular, in Rails apps you’re presented with a params hash from the user, and you’re supposed to write a function against it. Rails validates the params hash in a few ways:
- ActionPack forces the hash to have a general structure by construction: the hash only has strings as keys, and values can be strings, more hashes, arrays, and files.
strong_attributesdoes attribute whitelisting.
- ActiveModel does field validation.
This trio is usually pretty effective. However, there are some problems:
- The structure of the hash is frequently assumed to be correct. For instance, it’s taken for granted that
params[:user]is a hash, and
params[:user][:role_id]is a string (and not, say, an array)
attr_accessiblesuffers from context blindness: you’re frequently going to have an end user UI and an admin UI. You want admins to have access to more fields.
- It works best if you are mapping inputs directly to database fields.
Lastly, on our more beastly AR models, we started removing lifecycle callbacks and putting them into the service objects. This makes the AR models quite anorexic. The focus is on context: each Mutation operates in a single context, so there’s very little conditional logic. Each Mutation is a clear, linear sequence of code.
The Mutant Baby
And so were born Mutations! Simply, it lets you validate and specify inputs on your service objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
and is callable like this:
First, you specify the required and the optional inputs to the “function”. You also specify their type and constraints. If the inputs don’t match the constraints, and can’t be coerced to them, then the inputs are invalid and execute won’t be called. If everything checks out, then the execute method is called. Within execute, all of the inputs are safe, whitelisted values. You know all of the required values are present, and they’re the right types. This makes it a lot easier to write correct code against these inputs.
This looks pretty simple but it turns out to be pretty useful. There are a lot of synergies that arise and writing your application like this has a variety of benefits.
For complete documentation, see the README.
Reduce Code Duplication
By building an internal API (service layer) that your controllers can use, you’ll have less code duplication if you have multiple controllers that operate on the same types of data. This is straightforward and can be achieved by building any service layer, as I imagine many other folks using the Prime Stack have found.
Security & Correctness
One of the core benefits is security. There have been several Rails exploits in the past year (CVE-2013-0155 and CVE-2012-5664) that are related to an ‘incorrect datatype’ – you expected
params[:id] to be a string, but in fact it was an array. These are cases where there was a bug in the code due to lack of robustness, and causing that bug also happened to reveal a security issue. There are plenty of other cases where bugs of the same type – “type checking bugs” – are simply non-exploitable bugs.
Using Mutations decreases the likelihood of issues like this popping up. If you specify that a parameter must be a string, then inside the execute method, it is guaranteed to be a string. If an ignorant or malicious user passes in an array, then an error will be raised. If the parameter can be safely coerced to a string (eg, an Integer or boolean is passed; 3 becomes “3” and false becomes “false”), then the coercion is done and you have string. This makes it much easier to write correct code, and correct code is much more likely to be secure.
Reasoning About Logic
The benefit I’m most excited about is that it’s really easy to reason about a Mutation, because it only does one thing. In contrast, it’s more difficult to reason about callback soup.
With a Mutation, you have a single business rule that you’re coding against. You’re likely to have a short, linear method.
With fat models, first of all you don’t know what the possible business operations are. But let’s say you’re thinking about creation. You have to look at the callbacks, and figure out which ones will be called in this context. In some cases, you have to look at model instance variables, which encode which context you’re in. Sometimes, information is passed from one callback to another callback. And when you’re writing callbacks, you have to make sure your piece of code works correctly in all contexts.
Callbacks are great for small apps and for simple things that are context-insensitive. For moderately complicated models, callbacks are a terrible way to encode your business rules.
How often have you seen methods with signatures like this?
user and idea are clear, but what are valid options? If you’re lucky, do_task is a short function and you can see which keys and values can be in options. If you’re not, the method is implemented like this:
1 2 3
And now you’re on a wild goose chase. The standard solution to this is a comment above the method. But as dogma goes, code shouldn’t need comments because code should be clear already.
Mutations solves this fairly nicely: all “options” are documented as code, not as comments. It’s extremely clear what options can be, including their types and constraints. Example:
1 2 3 4 5 6 7 8 9 10 11 12 13
Notice that you know what is required, what is optional, what default values are, and what the data types of these fields are. This is more verbose than the original method, of course, but could be on-par with the original method plus a comment block.
Mutations help with form development in three ways:
Multi-model forms are handled well. The standard solution is to use
accepts_nested_attributes_for. Sometimes this works great, but other times you find yourself bending over backwards to do validations and other logic with callbacks. With Mutations, the structure of a nested hash can be well specified, and it’s easy to write logic against that instead of handing it over directly to ActiveRecord.
Forms where the input tags don’t map exactly to database fields are handled nicely. The standard solution is to add an
attr_accessorto the model, whitelist it, and then operate on it in callbacks. This sucks. It’s much nicer to specify this parameter as an input and operate on it in a simple method.
Dual client-side/server-side errors are painless with Mutations. When you run a Mutation against a user’s profile, for instance, this is what you can get back:
1 2 3 4 5 6
Mutations are great if you’re working on a bigger, long-lived application in a team setting. The service layer aspect gives you a scalable architecture. The input validation aspect makes it easy to pass user input directly into your service layer. You’ll have more robust and secure code.
To learn more, take a look at the Mutations home page.