Diminuindo a complexidade dos controllers no Ruby on Rails
Leia em 3 minutos
Não é de hoje que desenvolvedores tem problemas em manter a complexidade de seus projetos sob controle. No começo, tudo é bem organizado, os testes são rápidos e você sente prazer em modificar aquele projeto. Mas com o passar do tempo, muitos projetos começam a se deteriorar e logo você sente que podia ter feito um trabalho melhor. E não importa a linguagem ou framework que você utilize; esses problemas estão ligados ao modo como você escreve o código, e não quanto às características da linguagem/framework.
Já voltado ao Rails, você provavelmente já deve ter ouvido a expressão “fat model, skinny controller”, termo cunhado por Jamis Buck em um artigo de 2006. Neste artigo, ele demonstra como você pode reduzir a complexidade de views movendo algumas lógicas utilizadas pelo template para o model, neste caso, ActiveRecord.
E isso trouxe novos problemas. Agora, os modelos de ActiveRecord continham milhares de linhas, sempre com a premissa de que ter controllers com pouca responsabilidade implicava em ter modelos do ActiveRecord extremamente inchados. Acredito que este problema foi causado, provavelmente, por mostrar um exemplo superficial que usava o ActiveRecord. O ponto é que você deve fazer isso, mas não precisa necessariamente usar modelos do ActiveRecord.
Eu nunca estou satisfeito com a complexidade dos projetos que participo e sempre tento, em novos projetos, aplicar novos conceitos, experimentando, tentando atingir o nirvana. Neste artigo vou mostrar o modo como escrevo meus controllers, buscando a simplicidade máxima sem comprometer a produtividade e legibilidade.
Nosso caso de uso
Neste exemplo, iremos criar o controller de uma página de checkout, por exemplo do HOWTO. Veja o que deve acontecer quando um pedido é finalizado:
- Validar o nosso pedido, garantindo que as opções sejam válidas
- Processar o pagamento utilizando algum serviço
- Enviar um e-mail de confirmação para o cliente
- Redirecionar o cliente para uma página de confirmação
Nossa página de checkout será algo nessa linha:
O primeiro passo é criar o controller. Vou focar apenas no método CheckoutController#create
, já que é ele o responsável por lidar com o envio do formulário.
class CheckoutController < ApplicationController
def create
checkout = Checkout.process(checkout_params)
respond_with checkout, location: -> { order_path(checkout.order_id) }
end
private
def checkout_params
params
.require(:checkout)
.permit(:number_of_subscriptions, :payment_method, :name, :email, :tos)
end
end
Como você pode perceber, nosso controller é responsável apenas por passar os dados para a classe Checkout
. Simples assim.
Uma biblioteca essencial para mim é a gem responders. Com ela posso remover coisas como definição de flash messages e condicionais dependendo do estado do objeto de meus controllers.
A classe Checkout
precisa ser compatível com ActiveModel
. Para isso, basta usar o módulo ActiveModel::Model
. Vamos aproveitar para definir as propriedades e validações.
class Checkout
include ActiveModel::Model
attr_accessor :number_of_subscriptions, :payment_method, :name, :email, :tos
validates_numbericality_of :number_of_subscriptions, minimum: 1
validates_inclusion_of :payment_method, in: %w[paypal pagseguro]
validates_presence_of :name
validates_email_format_of :email
validates_acceptance_of :tos
end
Ao incluir o módulo ActiveModel::Model
, temos todas as caractísticas de validação, assim como o objeto Checkout#errors
de erros, além do método Checkout#valid?
. Temos também a capacidade de mass assignment na instanciação do objeto.
Agora, vamos criar o método Checkout.process
, responsável por efetuar o pedido caso ele seja válido. Ele irá apenas instanciar a classe Checkout
, executando o método Checkout#process
.
class Checkout
# ...
def self.process(attrs)
new(attrs).tap(&:process)
end
end
Vamos precisar implementar também o método Checkout#process
. Neste método, temos que criar o pedido e enviar o e-mail de confirmação.
class Checkout
# ...
def process
return unless valid?
save_order
send_confirmation_email
end
def order_id
order.id
end
private
def order
@order ||= Order.new(
number_of_subscriptions: number_of_subscriptions,
email: email,
name: name,
payment_method: payment_method
)
end
def save_order
order.save!
end
def send_confirmation_email
Mailer.order_confirmation(order_id).deliver_later
end
end
Agora, precisamos entender como a gem responders funciona. Toda vez que você utiliza o método respond_with
, o estado do objeto será verificado, assim como o formato da requisição (content negotiation). Caso o objeto seja válido, o redirecionamento será feito para a url especificada (ou para a URL do resource, se esta tiver sido definida no arquivo de rotas). Em caso de falha, ele irá renderizar a página dependendo da ação que você está (:edit
para a ação update
e new
para a ação create
). Se você precisar definir flash messages, pode fazer isso com o responder :flash
; basta configurar o arquivo app/controllers/application_controller.rb
como o exemplo abaixo:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
respond_to :html
responders :flash
end
Finalmente, você deverá definir as mensagens em seu arquivo de internacionalização, como no exemplo abaixo:
pt-BR
flash:
checkout:
create:
notice: Seu pedido foi realizado com sucesso!
Para saber mais sobre responders, leia um artigo que escrevi aqui no blog.
Finalizando
De um modo geral, é assim que crio meus controllers que precisam fazer mais de uma ação (como enviar e-mails caso o registro seja criado). Em casos mais simples, onde preciso apenas criar e redirecionar para uma página, simplesmente não preciso desta técnica. Basta fazer algo como isso:
class WorkshopsController < ApplicationController
def create
@workshop = Workshop.create(workshop_params)
respond_with @workshop, location: -> { edit_workshop_path(@workshop) }
end
private
def workshop_params
# ...
end
end
Você faz algo para simplificar os seus controllers? Compartilhe suas dicas nos comentários!