Separando a regra de negócio dos controllers (com Interactors e Organizers)

Neste post você verá como refatorar controllers gordos (fat controllers) em aplicações Rails, extraindo a regra de negócio para classes especializadas com Interactors e Organizers.O objetivo é mostrar, com um exemplo real, como essa separação traz um código mais limpo, testável e fácil de manter. MVC Grande parte das aplicações…

Neste post você verá como refatorar controllers gordos (fat controllers) em aplicações Rails, extraindo a regra de negócio para classes especializadas com Interactors e Organizers.
O objetivo é mostrar, com um exemplo real, como essa separação traz um código mais limpo, testável e fácil de manter.

MVC

Grande parte das aplicações web segue o padrão arquitetural MVC — Model, View, Controller.

O padrão se propõe a separar e delegar o fluxo sobre essas três camadas:

  • Model – concentra a lógica de domínio, regras de negócio e persistência de dados.
  • View – cuida da apresentação da informação para o usuário (HTML, JSON, etc.).
  • Controller – é a ponte entre a requisição externa e o núcleo da aplicação.

Controller

Recebe uma chamada HTTP, interpreta seus parâmetros e coordena o que deve acontecer no domínio da aplicação, retornando uma resposta adequada.

Do contrário, não é responsabilidade do controller:

  • Implementar regras de negócio complexas de domínio.
  • Fazer integrações externas pesadas (ex.: chamadas a APIs, processamento de arquivos) diretamente.
  • Manipular dados em grande escala ou manter lógica de persistência elaborada.

Quando um controller começa a acumular responsabilidades como essas, surge o famoso “fat controller”.

# app/controllers/newsletters_controller.rb
class NewslettersController < ApplicationController
  def signup
    email = params[:email]
    
    if email.present? && valid_email?(email)
      existing_newsletter = Newsletter.find_by(email: email)
      
      if existing_newsletter
        if existing_newsletter.unsubscribed_at.present?
          existing_newsletter.update!(
            unsubscribed_at: nil,
            resubscribed_at: Time.current
          )
          message = 'Assinatura reativada com sucesso!'
        else
          # Email já ativo
          return redirect_to root_path, alert: 'Este email já está cadastrado!'
        end
      else
        existing_newsletter = Newsletter.create!(
          email: email,
          subscribed_at: Time.current
        )
        message = 'Cadastro realizado com sucesso!'
      end
      
      if existing_newsletter.resubscribed_at
        NewsletterMailer.reactivation_email(existing_newsletter).deliver_now
      else
        NewsletterMailer.welcome_email(existing_newsletter).deliver_now
      end
      
      redirect_to root_path, notice: message
    else
      redirect_to root_path, alert: 'Email inválido!'
    end
  end

  private

  def valid_email?(email)
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
end

O que tem de errado aqui?

O controller “sabe demais” sobre o fluxo de negócio.

O chefe de restaurante “faz tudo”

Imagine um restaurante em que um único chefe:

  • Recebe o cliente na porta
  • Verifica se tem vaga
  • Volta para informar
  • Leva eles para a mesa
  • Anota o pedido
  • Prepara todos os pratos
  • Cuida do caixa
  • Limpa as mesas
  • Vai ao estoque repor ingredientes

Como deveria ser:

  • Recepcionista → recebe clientes e anota pedidos.
  • Chefs → cozinham cada tipo de prato.
  • Caixa → cuida do pagamento.
  • Equipe de limpeza → mantém o ambiente pronto.

Interactor

Um Interactor é uma classe que encapsula um caso de uso específico da aplicação, concentrando toda a regra de negócio necessária para executar essa ação.
Ele segue o princípio de Single Responsibility: cada interactor faz apenas uma coisa, de ponta a ponta, e expõe um método principal (call) para ser usado pelo controller.

Como funciona?

  • Você define uma classe que inclui Interactor.
  • O método call recebe o contexto (context) com os dados de entrada.
  • Se algo der errado, você pode encerrar o fluxo com context.fail!.
  • No final, você grava no context as saídas que o controller precisa.

Context

Interactors funcionam com context. Pode ser entendido como um cofre, onde os dados são armazenados e podem ser acessados.

No nosso Gemfile:

gem "interactor", "~> 3.0"

Estrutura de pastas:

▾ app
  ▸ controllers
  ▸ helpers
  ▾ interactors
      newsletter
	      signup.rb
  ▸ mailers
  ▸ models
  ▸ views

Interactor:

# app/interactors/newsletter/signup.rb
class Newsletters::Signup
  include Interactor

  def call
    email = context.email

    unless email.present? && valid_email?(email)
      context.fail!(message: 'Email inválido!')
    end

    newsletter = Newsletter.find_by(email: email)

    if newsletter
      if newsletter.unsubscribed_at.present?
        newsletter.update!(
          unsubscribed_at: nil,
          resubscribed_at: Time.current
        )
        NewsletterMailer.reactivation_email(newsletter).deliver_later
        context.message = 'Assinatura reativada com sucesso!'
      else
        context.fail!(message: 'Este email já está cadastrado!')
      end
    else
      newsletter = Newsletter.create!(
        email: email,
        subscribed_at: Time.current
      )
      NewsletterMailer.welcome_email(newsletter).deliver_later
      context.message = 'Cadastro realizado com sucesso!'
    end

    context.newsletter = newsletter
  end

  private

  def valid_email?(email)
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
end

Controller:

class NewslettersController < ApplicationController
  def signup
    result = Newsletter::Signup.call(email: params[:email])
    
    if result.success?
      redirect_to root_path, notice: result.message
    else
      redirect_to root_path, alert: result.message
    end
  end
end

Melhorias:

  • O controller volta a ser apenas a ponte entre requisição e resposta.
  • Toda a política de negócio vive no Interactor.

Organizer

À medida que o sistema cresce, um único interactor pode voltar a ficar “gordo”.
Quando a lógica de negócio se desdobra em vários passos independentes, a gem interactor oferece um recurso perfeito: o Organizer.

Compartilhando o context

O Organizer é um interactor que orquestra outros interactors.
Em vez de escrever toda a lógica em um único call, você cria pequenos interactors especializados (ex.: validar dados, criar ou atualizar registro, enviar e-mail) e o Organizer executa cada um em sequência, compartilhando o contexto entre eles.

Esse padrão ajuda a manter alta coesão e baixo acoplamento, além de deixar o fluxo de negócio explícito.

Quando usar

  • O caso de uso já tem múltiplas responsabilidades distintas.
  • Existe possibilidade de crescimento: novos passos, integrações externas, métricas, double opt-in etc.
  • reuso de passos em outros fluxos.
  • Sequência determinadas de passos dependentes

Exemplo:

Refatorando o interactor Newsletters::Signup em vários passos:

class Newsletters::ValidateEmail
  include Interactor
  def call
    email = context.params[:email].to_s.strip
    unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      context.fail!(message: 'Email inválido!')
    end
    context.email = email
  end
end
class Newsletters::UpsertSubscription
  include Interactor
  def call
    newsletter = Newsletter.find_by(email: context.email)
    if newsletter
      if newsletter.unsubscribed_at.present?
        newsletter.update!(unsubscribed_at: nil, resubscribed_at: Time.current)
        context.action = :reactivated
      else
        context.fail!(message: 'Este email já está cadastrado!')
      end
    else
      newsletter = Newsletter.create!(email: context.email, subscribed_at: Time.current)
      context.action = :created
    end
    context.newsletter = newsletter
  end
end
class Newsletters::SendEmail
  include Interactor
  def call
    case context.action
    when :reactivated
      NewsletterMailer.reactivation_email(context.newsletter).deliver_later
      context.message = 'Assinatura reativada com sucesso!'
    when :created
      NewsletterMailer.welcome_email(context.newsletter).deliver_later
      context.message = 'Cadastro realizado com sucesso!'
    end
  end
end

O Organizer orquestra esses passos:

class Newsletters::SignupOrganizer
  include Interactor::Organizer
  organize Newsletters::ValidateEmail,
           Newsletters::UpsertSubscription,
           Newsletters::SendEmail
end

E o controller permanece enxuto:

class NewslettersController < ApplicationController
  def signup
    result = Newsletters::SignupOrganizer.call(params: params)
    if result.success?
      redirect_to root_path, notice: result.message
    else
      redirect_to root_path, alert: result.message
    end
  end
end

Benefícios

  • Código modular: cada interactor é pequeno e de responsabilidade única.
  • Alta testabilidade: é possível testar cada passo isoladamente.
  • Facilidade de evolução: adicionar uma etapa (ex.: analytics, CRM) significa apenas criar um novo interactor e adicioná-lo ao organize.
  • Clareza: o fluxo de negócio é visível em uma linha no Organizer.

Documentação

0 Comentário