How I Design Rails Applications Part 2: Form Objects
In part 2 of this series I am going to discuss the use of form objects. Just as in the last installment I drew my inspiration from this article by Bryan Helmkamp. As a warning, I believe the form object is probably the most misunderstood pattern contained in Bryan’s article. One needs to immediately decouple the form object from the concept of a UI form (more on this later).
What is a Form Object and Why Should I Use It?
A form object is the abstraction of operations that should occur when saving a record within a system, business logic if you will. The form object provides many benefits, some of which are:
- Abstracts the responsibility of create/update of a model(s) to some other class for a better separation of concerns
- A layer of indirection between your application and the persistence library being used
- Eliminates validation spaghetti (a la ActiveRecord validations, etc)
- Allows for more control of how to save associated (nested) objects
While ORMs like ActiveRecord provide a means to persist to a DB by generating SQL, validate attributes, accepts_nested_attributes_for in order to save associated “nested” objects, and many other features, it does not mean you should use them all. An ORM should know how to read/write data to a database, period. Once you start layering all of these other concerns into the “model,” you get yourself into trouble quickly. I believe that most of the pain I have experienced with mature Rails applications is a result of this overlaying of concerns in a single class.
Abstracts the responsibility of create/update of a model(s) to some other class for a better separation of concerns
A form object can be considered a type of decorator for your models. Just as you may use a presenter (a type of decorator) for logic that is not directly related to your data model but more for presentation, you would use a form object for functionality that is not directly related to your data model, but more to the business logic you would like to include in the create/update operations for your model.
Specific examples of this business logic are validation of attributes, creation of associated (nested) records, etc.
A layer of indirection between your application and the persistence library being used
Providing a layer (adapter) between your application and ORM is something that most people scoff at due to the unlikeliness of needing to change ORMs in the lifetime of a project. I myself might have subscribed to this line of thought a mere year ago. However, I have experienced the bite of this issue first hand now. A project I am working on had a requirement for a distributed data model injected late in the project. After analyzing how hard it would be to move from a RDBMS to a document database that has eventual consistency out of the box, like Cassandra, we determined that we could not achieve the refactor of the persistence layer in time to deliver the project. The reason for this is the application is tightly coupled to the persistence layer. If we simply had one level of indirection built into the persistence layer using something like form and query objects, we could have easily acheived a move to Cassandra.
Many developers employ the data repository pattern in order to provide a level of indirection between their application logic and ORM classes. An alternaive to this pattern is to use form and query objects. I will cover query objects in the next installment of this series. I prefer for and query objects over a data respository because the repository usually grows to be a rather large monolithic class in itself. Form and query objects allow for the encapsulation of a single domain operation per class.
Eliminates validation spaghetti (a la ActiveRecord validations, etc)
The easiest way to explain this is through an example. Given a user model that is used to register a new user and also to manage the user’s profile.
class User < ActiveRecord::Base attr_accessor :registering validates_presence_of :password, if: :registering? validates_presence_of :password_confirmation, if: :password_present? protected def registering? registering end def password_present? password.present? end end
Even in this simple example you can see how it get hard to determine when valdiations will execute. Imagine adding more concerns/features that the user model is handling with even more conditional validations. The object quickly becomes brittle and even a slight change could result in lots of broken specs.
Why does all of this logic have to live in a single class? The following is a better implementation.
class User < ActiveRecord::Base end class UserRegistrationForm < Reform::Form # from gem apotonick/reform property :email property :password property :password_confirmation validates :email, :password, :password_confirmation, presence: true end
When you are registering a user, you can use the UserRegistrationForm. If you need a form for updating the user’s additional profile elements you might implement a form like the following.
class UserProfileUpdateForm < Reform::Form # from gem apotonick/reform property :name property :phone_number property :age validates :age, numericality: true end
Each of the form objects above implements the business rules necessary to perform a create/update operation within a specific context. This makes the business logic contained in each much more explicit and easier to follow.
Allows for more control of how to save associated (nested) objects
Persisting complex object graphs through a single method call is a highly desirable level of abstraction for any application. However, ActiveRecord’s accepts_nested_attributes_for feature is an abomination and should be deprecated from ActiveRecord. Why should a model be concerned with how to persist its associated models?
Form objects can also help clean up this complex task. Given a project that has many tasks you may implement a form for project creation that allows for one or more initial tasks to be defined.
class Project < ActiveRecord::Base has_many :tasks end class Task < ActiveRecord::Base belongs_to :project end class ProjectCreationForm < Reform::Form # from gem apotonick/reform property :name validates :name, presence: true property :due_on collection :tasks do property :description validates :description, presence: true property :due_on end def save Project.transaction do # save project # save tasks end end end
The above implementation is much more explict and much less magic involved than accepts_nested_attributes_for. If there is a problem, it will be much easier to track down what is going on. Not to mention, when using the ProjectCreationForm we can be sure that only a project and one or more tasks can be created. With accepts_nested_attributes_for, we could accidentally create other associated objects if our attributes hash happened to have stray attributes in it providing an opportunity for a bug that would be hard to track down. This is due to smashing too many concerns into a single class. The more concerns that build up in the class, the more opportunites that bugs will occur.
Form Objects are Not Always Used with a UI Form
The sooner one realizes that a form object is not always used in conjunction with a UI form the quicker one will begin to understand the form object’s true place within a system. Following are some of the situations in which you might use a form object:
- In the Rails console when you want to create a valid record
- In the create or update action for a REST API
- When importing records from a flat file
If you use forms correctly there is no longer a need to define any validations on your ActiveRecord model classes. If this makes you feel uncomforatble, ask yourself why? To truly have a layer of indirection between the application and ORM, you should only directly use the ActiveRecord model layer within the form and query objects when employing this pattern.
The Reform Gem
When implementing form objects, I prefer to use the reform gem by apotonick. It already has a very well thought out and designed API and it is still a pre 1.0 release. A bonus feature with this gem is it plays nicely with Rails’ built-in form builders and 3rd party form builders formtastic and simple_form. You can pass the form object to these builders in place of the model and everything will work as expected.