Note: The current article is based on Ruby on Rails examples, but the core idea should be applicable to other frameworks/languages.
Let’s start trying to define some concepts:
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:
As you can see a form object is a broad, non-uniform idea. There’s no consensus on what they are, or when to use them or what their responsibilities are.
And 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).
Most of the Form Objects that I came across with 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?
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
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:
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.
There’s yet another other problem and perhaps the more concerning one: you probably already have objects that are in charge of these responsibilities.
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 component’s 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.
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
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 definitely 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 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 it would be good for your codebases, let us know!