On October 17, 2012, Bryan Helmkamp, founder of Code Climate, wrote a blog post outlining 7 patterns to refactor fat ActiveRecord models in Ruby on Rails. Here at Crush & Lovely, this post is a core reference for all Rails developers on how to separate concerns, write modular, concise and expressive code, and make testing exceedingly simple.
Forms often have complex logic applied to them. In general, the logic breaks down into the following categories: validation, persistence or other operations, and feedback
A Form Object can encapsulate all associated logic into a single object, keeping it focused, isolated, and easy to test. You may have a sign up form that creates an account, so an associated Form Object could handle the following logic:
- Make sure all fields that are required are present
- Make sure all values are valid
- Persist the data to the database
- Provide success or error feedback to the user
Placing model validations on the Form Object instead of on a centralized model is perhaps counter-intuitive, since you may have to repeat these validations across multiple Form Objects that affect the same model. One question to ask is: where do you want the guards placed in the life-cycle of a form submission? Personally, I favor keeping validations closer to the form than the database, so we can provide quicker feedback. It also feels more semantic that the validations should be just on the other side of the submit click instead of deep in a model definition. This also gives you fine-tuned control over the validations in their specific context and not guard against all scenarios on the model.
In fact, if we think about it on a higher level, we are in a way off-loading the concept of a "Model" to the Form Objects and treating our "Model" objects like Data Access Objects, or DAOs. If this is to be true, there has to be a bond of trust between the Model and the Form Object that what is being sent to the model is pure. From an application architecture standpoint, this can be a really nice design pattern.
Let's take a look at two examples, one demonstrating a full Form Object that covers all form operations and one that is a Validation Object that can be sequenced with other components.
Let's imagine a teacher is registering new students for the school year. The application can hand the form data off to the Form Object for handling all aspects of its processing flow:
This form gives us a short, expressive API for executing this form in our main application components, like in a controller or a client-side view:
If we think creatively about the composition of Form Objects, we can create a consistent API on all sides of the application. For example, if instead of a Form Object that encompasses all aspects of form processing, we create a Validation Object that only guards form values, we can use it to compose consistent, expressive and context-specific process flows:
This approach is nice because we can start to favor flexible composition over large, monolithic objects.
You see how we defined the Validator Object once and can use it in each entry point to the database, guarding it consistently across all fronts. This approach can help keep things DRY and organized, but if you find it easier to see everything in one (potentially large) Form Object instead of having the process divided up between specialized components, that's completely valid, too. It's all about what type of composition feels best to you and your team.
No matter how you compose your Form Objects, testing is made simpler by extracting it out of the application stack. All you have to do is compose an object composed of the form data you want to test, and send it through. It's a good practice to make sure you test the error handling, too, to make sure that all applicable errors are sent back to the application for messaging to the user.
In the next post, we'll take a look at Query Objects, giving us a really expressive and clean way of either retrieving records from the database or filtering down a collection.