Code simplicity - Command pattern

September 01, 2017 | Nicolas Zermati | 10-minute read

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:

  1. validate that the payment amount is correct,
  2. pay the order,
  3. create an invoice using the existing sales quote,
  4. update the state of the order, and
  5. send notifications.
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.

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.

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:

  • 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…

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.

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:

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!

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!

View openings 👍  Like this post? Join Drivy's engineering team!