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.
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:
class OrdersController < ApplicationController
def confirm
order = current_user.orders.find(params.fetch(:id))
payment = order.payments.create!(token: params.fetch(:payment_token))
if payment.amount != order.sales_quote.amount || payment.currency != order.sales_quote.currency
raise Payment::MismatchError.new(payment, order)
end
payment.capture!
Order.transaction do
invoice = order.invoices.create!({
amount: order.sales_quote.amount,
currency: order.sales_quote.currency,
})
@order.sales_quote.items.find_each do |item|
invoice.items.create!({
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price,
})
end
order.update(status: :confirmed)
end
OrderMailer.preparation_details(order).deliver_async
OrderMailer.confirmation(order).deliver_async
OrderMailer.available_invoice(order).deliver_async
flash[:success] = t("orders.create.success")
redirect_to invoice_path(invoice)
rescue Payment::MismatchError
flash[:error] = t("orders.create.payment_amount_mismatch")
redirect_to :back
rescue Payment::CaptureError
flash[:error] = t("orders.create.payment_capture_error")
redirect_to :back
rescue ActiveRecord::RecordInvalid
flash[:error] = t("orders.create.payment_token_invalid")
redirect_to :back
end
end
There are more or less obvious issues in that implementation. Let’s see how much extracting that logic could help.
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.
class OrdersController < ApplicationController
def confirm
ConfirmOrder.new(
Order.find params.fetch(:id),
params.fetch(:payment_token)
).perform
flash[:success] = t("orders.create.success")
redirect_to invoice_path(invoice)
rescue Payment::MismatchError
flash[:error] = t("orders.create.payment_amount_mismatch")
redirect_to :back
rescue Payment::CaptureError
flash[:error] = t("orders.create.payment_capture_error")
redirect_to :back
rescue ActiveRecord::RecordInvalid
flash[:error] = t("orders.create.payment_token_invalid")
redirect_to :back
end
end
class ConfirmOrder
def initialize(order, payment_token)
@order = order
@payment_token = payment_token
end
def perform
payment = @order.payments.create!(token: @payment_token)
if payment.amount != @order.sales_quote.amount || payment.currency != @order.sales_quote.currency
raise Payment::MismatchError.new(payment, @order)
end
payment.capture!
Order.transaction do
invoice = @order.invoices.create!({
amount: @order.sales_quote.amount,
currency: @order.sales_quote.currency,
})
@order.sales_quote.items.find_each do |item|
invoice.items.create!({
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price,
})
end
@order.update(status: :confirmed)
end
OrderMailer.preparation_details(@order).deliver_async
OrderMailer.confirmation(@order).deliver_async
OrderMailer.available_invoice(@order).deliver_async
end
end
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:
ConfirmOrder
in another context,ConfirmOrder
itself, this is an important-enough context to mention,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…
class ConfirmOrder
def initialize(order, payment_token, notify: true)
@order = order
@payment_token = payment_token
@notify = notify
end
def perform
# Same code as before...
if @notify
OrderMailer.preparation_details(@order).deliver_async
OrderMailer.confirmation(@order).deliver_async
OrderMailer.available_invoice(@order).deliver_async
end
end
end
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.
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:
class OrdersController < ApplicationController
def confirm
ConfirmOrder.new(
Order.find params.fetch(:id),
params.fetch(:payment_token)
).perform!
flash[:success] = t("orders.create.success")
redirect_to invoice_path(invoice)
rescue Command::Error => error
flash[:error] = t("orders.create.#{error.code}")
redirect_to :back
end
end
class ConfirmOrder < Command
def initialize(order, payment_token, notify: true)
@order = order
@payment_token = payment_token
@notify = notify
end
validate do
payment = @order.payments.new(token: @payment_token)
if !payment.valid?
add_error(:payment_token_invalid)
end
if payment.amount != @order.sales_quote.amount || payment.currency != @order.sales_quote.currency
add_error(:payment_amount_mismatch)
end
end
perform do
@order.payments.create!(token: @payment_token).capture!
Order.transaction do
invoice = @order.invoices.create!({
amount: @order.sales_quote.amount,
currency: @order.sales_quote.currency,
})
@order.sales_quote.items.find_each do |item|
invoice.items.create!({
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price,
})
end
@order.update(status: :confirmed)
end
if @notify
OrderMailer.preparation_details(@order).deliver_async
OrderMailer.confirmation(@order).deliver_async
OrderMailer.available_invoice(@order).deliver_async
end
rescue Payment::CaptureError
add_error(:payment_capture_error)
end
end
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!
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.
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!