A case against form objects
Note: The current article is based on Ruby on Rails examples, but the core idea should apply to other languages and frameworks.
The first
Let’s start trying to define some concepts:
What’s a form object?
What’s their architectural purpose?
According to the articles linked at the bottom and our own experience working on several Rails codebases using form objects, there’s a variety of definitions and objectives. To enumerate a few:
What are they? what do they do?
A plain old ruby object that runs validations on data input
Virtual models that represent the aggregation of other model objects
Replacement for strong parameters (input params whitelist)
A way to refactor model life-cycle callbacks
A form object is an object designed specifically to be passed to form_for
This type of object is used to make the life of a controller a bit easier and take params processing responsibility out of it. Creating form objects means making proper type coercions and introducing simple validations
Encapsulating the aggregation of multiple ActiveRecord models might be updated by a single form submission
The Form Object quacks like an ActiveRecord, so the controller remains familiar
An object that lets you manage complex behavior easily
Why use them? What’s their main purpose?
Extracting business logic from controller and/or model layers
Providing view-backing helper methods (eg: providing options for complex selects)
Easily implementing Rails conventions on complex forms that do not map directly into a single AR model
Moving responsibilities to the form object reduces decoupling between the controller, model (including multiple models), and the view template.
As you can see a form object is a broad, non-uniform idea. There’s no consensus on what they are, when to use them, or what their responsibilities are.
As you can see a form object is a broad, non-uniform idea. There’s no consensus on what they are, when to use them, or what their responsibilities are.
This is the first problem: the Form Object concept is communicatively poor. When I need to work on a codebase that uses form objects, I don’t know which of all these ideas I will find behind them, or, more frequently, what mix of them.
As a summary (and keep this idea in mind as we will review it later) we can say that the general idea is that form objects are a good way to refactor code complexity and condensation of responsibilities either in some part of the system (usually the model and/or controller layers).
An example
Most of the Form Objects that I came across are used like this. And according to the articles linked below, it might be considered the most popular implementation:
class SomethingController < ApplicationController
def create
@form = MyForm.new(action_params)
if @form.valid?
@form.save!
redirect_to "somewhere"
else
render :new
end
end
def new
@form = MyForm.new
end
end
This looks like almost every “rails-way” controller (if valid? save & redirect; else: render). Exactly what a rails dev expects to see.
Short & sweet, straight to the point.
This must be OK, right?
The second
Let’s analyze what the form object is in charge of in these few lines, what knowledge it manages only by taking a look at its public API
new(params) => knows the request’s params. This makes it a good place to house input data transformations to adapt it to the model’s needs.
valid? => The form runs validations. This means it knows the parameters, its types, and the validation rules, which also means knowledge of business logic and domain rules.
save! => Object persistence. That means that it also knows how to create them from parameters and how the persistence layer works.
@form => Variable passed to the view layer. Makes it a potential place to hold view-backing logic (eg: form_forchoices for selects and check/radios, conditional rendering, form_for model, etc)
Deep inside, in a shady corner, our single responsibility principle intuition, lonely cries Yeah… is it perhaps too much? An object that knows too much does too much. This exerts a gravitational pull of code on the whole system:
I need a few more view-backing helpers, where should I put them? => in the form object.
new validations? => form object.
Need to map params to some model attributes/objects => yeap, the form object.
Even worse if you give it as one of the posts says the opportunity to send mail and/or user notifications.
And the worst part is, and here comes the second problem: it does make sense to put those in there.
Changes like these will probably pass code review as there’s no good argument against them.
Our form object is now the new fat controller/model. It holds and attracts the same problems that we were trying to solve when we first introduced them.
The third
There’s yet another problem and perhaps the more concerning one: you probably already have objects that are in charge of these responsibilities.
persistence: it is a model’s responsibility. Delegate to those classes instead of imitating them. Making the controller look like a “blog-in-10-minutes” controller is not enough as an argument.
business logic: to hold business logic use service objects. They are a natural place for complex business logic.
input validation: use validation objects. There are multiple libraries you can use to implement them (including ActiveRecord::Model, Scrivener, and dry-schema)
view backing/helper methods: use a view model/presenter object.
If you are shooting for a form object, you probably have a medium-sized app and already have some of these in your codebase. Why add yet another architectural concept that has a rather vague definition and overlaps over other components’ responsibilities?
We need to embrace the idea that if the use case is complex, the code will be complex. Brushing the complexity under the carpet doesn’t make it disappear.
A proposal
Using the ideas above, the same controller could look like:
def new
@form = SomeFormViewModel.new(defaut_values)
end
def create
validation = SomeValidation.new(action_params)
if validation.valid?
result = SomeService.new(validation.params).call
if result.success?
redirect_to "somewhere", info: "everything went ok."
else
redirect_to "somewhere", error: "uh oh… something gone oops: # {result.errors}"
end
else
@form = SomeFormViewModel.new
render :new
end
end
PROS
It is not 100% rails-way but the structure is pretty similar and easy to follow
The controller is composing and delegating logic to single-purposed objects.
The limits and purpose of each piece are well-defined.
Each piece has a single focus, so they are easier and faster to test (and to maintain those tests)
The code structure and responsibilities distribution are self-replicating. Is easier to build a culture on top of these and it should also be harder for a change that mixes up responsibilities to pass code review.
Conclusion
This is an idea that arose out of an analysis of some of the codebases we inherited or built ourselves in the past and we now need to maintain. This is most likely not the only solution, and it is not a “default recipe” that should be followed blindly.
Form objects are not bad per se. Their intention is good and they are usually better than a bloated model or controller. Perhaps the next time you see one or think of adding a new one, some of these ideas come to your mind.
If you find anything useful or that you think would be good for your codebases, let us know!