Aprenda / Ruby on Rails

FundamentosAula 7 de 2016 min0% da trilha

Service objects

Quando uma ação envolve mais de um model, ela não pertence nem ao controller nem a um model só.

"Finalizar um pedido" não é só Pedido.update: baixa estoque, cria um registro de pagamento, manda e-mail de confirmação. Colocar tudo isso direto no controller (um "fat controller") mistura HTTP com regra de negócio; colocar no model Pedido mistura persistência com orquestração de várias tabelas. Um service object é uma classe cuja única responsabilidade é essa ação.

app/services/finalizar_pedido.rb
class FinalizarPedido
  def initialize(pedido)
    @pedido = pedido
  end

  def call
    return false unless @pedido.itens.any?

    ActiveRecord::Base.transaction do
      @pedido.update!(status: :finalizado)
      @pedido.itens.each { |item| item.produto.decrement!(:estoque, item.quantidade) }
      Pagamento.create!(pedido: @pedido, valor: @pedido.total)
    end

    PedidoMailer.confirmacao(@pedido).deliver_later
    true
  end
end
uso no controller — controller volta a ser fino
def finalizar
  pedido = Pedido.find(params[:id])
  if FinalizarPedido.new(pedido).call
    render json: pedido
  else
    render json: { error: "Pedido sem itens" }, status: :unprocessable_entity
  end
end

Note o ActiveRecord::Base.transaction: se Pagamento.create! falhar depois do estoque já ter sido baixado, a transação desfaz tudo — sem isso, um erro no meio do processo deixaria o banco num estado inconsistente (estoque baixado, mas sem pagamento registrado).

Quando NÃO criar um service

Uma action que só salva um registro (Produto.create(params)) não precisa de service — isso é exatamente pra que o model e o controller já servem. Service object resolve orquestração entre múltiplas entidades, não é uma camada obrigatória em toda action.

Exercício

Um CancelarPedido precisa: reverter o status pra "cancelado", devolver o estoque de cada item, e só funcionar se o pedido ainda não tiver sido entregue. Esboce a classe.

Quiz rápido

Quando faz sentido extrair um service object?
Por que envolver as operações em ActiveRecord::Base.transaction?