September 01, 2017 –
Nicolas Zermati
–
10-minute read
This article was written before Drivy was acquired by Getaround,
and became Getaround EU. Some references to Drivy may therefore remain in the post
The command pattern is sometimes called a service object, an operation, an action,
and probably more names that I’m not aware of. Whatever the name we gave it,
the purpose of such a pattern is rather simple: take a business action and put it
behind an object with a simple interface.
A controller’s action doing it all
One of the most common use case I encounter for this pattern is to get business
logic out of MVC’s controllers. For instance, in a Rails application, an action
responds to a single HTTP call using a POST, a PATCH, or a PUT verb and
semantic. It means that those actions are intended to update the application’s
state.
The following example takes an action to illustrate the situation. The goal of
the confirm action is to complete an order. Completing an order follow those
steps:
validate that the payment amount is correct,
pay the order,
create an invoice using the existing sales quote,
update the state of the order, and
send notifications.
There are more or less obvious issues in that implementation. Let’s see how much
extracting that logic could help.
Moving out
The first step of the extracting process is simple: take the content of the action, put
it in an object and call this object from the controller.
It seems to be more complicated than before. In some way it is since there is one
extra level of indirection to the ConfirmOrder object now. Despite that, this
basic extraction provides interesting benefits such as:
focusing the controller on fetching the parameters and handling the response,
reusing the ConfirmOrder in another context,
testing ConfirmOrder itself, this is an important-enough context to mention,
sharing behavior between commands, and
getting some privacy.
Supporting multiple contexts
Reusing this ConfirmOrder in a different context is easy. It is a small amount
of work to get variations. Imagine that, because of the context, you want to
confirm an order without sending the notifications…
We could use some state machine’s hook in order to deliver notifications and even
to create the invoice. I tend to avoid callback as much as possible. Encapsulating
the behavior in an object allows us to see what’s going on during that action in
the same file. Also, it is easy to tweak the behavior if needed without impacting
the rest of the system, as we just did.
Sharing behavior
Many business actions, such as completing an order, can be extracted using this
pattern. Giving a clean API to all those commands gives some structure and
consistency to the codebase.
In the example, the errors mechanism is using exceptions, such as Payment::CaptureError,
forcing the controller to know about each one of them. At Drivy we’ve built a
validation layer allowing us to write:
You may be able to guess what’s in the Command class but there isn’t much.
Also here I’m using the Ruby 2.5 rescue which will work inside blocks!
Wait, what did you meant by privacy?
When all that code was in the controller, it was surrounded by other actions. It
means that each private method that you would like to define would also be visible
from within those other actions. Most of the time it doesn’t make sense. I’ve seen,
and unfortunately wrote myself, controllers with too many private methods. I dodged
the name clashes with prefixes, I grouped methods by the action they referred to, I
added comments, and I even tried concerns. Nothing really was really satisfying.
In that sense, a dedicated object make things a lot simpler to organize. In the next
article of the serie, I’ll go deeper on how to use and abuse methods in order
to offer the best documentation to the next developer. It’ll continue this example
so be sure to check it out.
Conclusion
In this article nothing is especially new but this way of bundling business actions
is getting more and more common. Hanami has Action and
Trailblazer has Operation for instance. If you never thought of it,
it is time to practice!
Did you enjoy this post? Join Getaround's engineering team!