Skip to content
novembro 1, 2010 / cassiomarques

Usando filter-objects para organizar melhor o códigos dos controllers

O recurso de filtros do Rails é bem útil. É difícil encontrar controllers em apps Rails que não possuam pelo menos um filtro sendo executado. Às vezes esses filtros são simples, como os filtros para verificar permissões ou se o usuário está autenticado. Entretanto muitas vezes a lógica envolvida em um filtro é complexa. Nestes casos é fácil perder o controle e começar a escrever filtros extremamente longos e complexos, poluindo nossos controllers. Além de bagunçado, começa a ficar bem difícil testar esses filtros.

Imagine o seguinte controller:

class FoosController < ApplicationController
  before_filter :do_something_complex

  protected
  def do_something_complex
    # dezenas de linhas ...
  end
end

Esse é o esquema padrão de escrever filtros em Rails. Mas pode ficar ainda pior…

class FoosController < ApplicationController
  before_filter :do_something_complex

  protected
  def do_something_complex
    check_whatever
    destroy_the_world
  end

  def check_whatever
    # dezenas de linhas ....
  end

  def destroy_the_world
    # dezenas de linhas...
  end
end

Haja código bagunçando nosso controller!

Filtros como objetos

O Rails permite o uso de classes como filtros, para tirar a lógica do controller. Algo mais ou menos assim:

class FoosController < ApplicationController
  before_filter MyAwesomeFilter  # continua aceitando as opções :only e :exclude
end

class MyAwesomeFilter
  def self.filter(controller)
    # ...
  end
end

Já é melhor do que entupir nosso controller com código que não deveria estar ali, mas ainda não é o ideal, visto que usa métodos de classe, transformando nosso filtro em nada mais do que um namespace para funções procedurais. O fato é que o before_filter aceita qualquer objeto que responda ao método filter(controller) (lembre-se que em Ruby uma classe é também um objeto!). Podemos melhorar o design fazendo algo como:

class FoosController < ApplicationController
  before_filter MyAwesomeFilter.new 
end

class MyAwesomeFilter
  def filter(controller)
    # sua lógica, mas seja bacana e refatore!
  end
end

Bem melhor! Mas ainda temos um problema. Nosso filtro é uma classe comum e não possui acesso direto à nada disponível dentro do nosso controller, pois o escopo aqui é outro. A prática comum é usar a instância do controller que o método filter recebe:

class MyAwesomeFilter
  def filter(controller)
    SomeObject.run controller.params[:foobar]  
    controller.flash[:notice] = "Some message"
    controller.redirect_to some_path
  end
end

Estamos delegando um monte de chamadas para o nosso controller, mas acho que não é necessário escrever controller tantas vezes. Vamos delegar da maneira Ruby:

require 'forwardable'
class MyAwesomeFilter
  extend Forwardable

  def_delegators :@controller, :redirect_to, :render, :params, :flash # .. e qualquer outro método que você queira 

  def filter(controller)
    @controller = controller
    flash[:notice] = "Some message"
    redirect_to some_path
  end
end

Com o module Forwardable podemos declarar quais chamadas serão delegadas e para quem. Ele é nativo do Ruby, dê uma olhada na documentação.

Mas e se formos criar outros filtros? Vamos ter que declarar os delegators novamente? Não!

require 'forwardable'
class MyBaseFilter
  extend Forwardable

  def_delegators :@controller, :redirect_to, :render, :params, :flash

  def filter(controller)
    @controller = controller
  end
end

class MyAwesomeFilter < MyBaseFilter
  def filter(controller)
    super(controller)

    # sua lógica de filtro
  end
end

Com essa estratégia, você pode criar outros delegators usados somente em um filtro específico. Esses delegators serão somados aos já declarados na classe base:

class MyAwesomeFilter < MyBaseFilter
  def_delegators :@controller, :foo, :bar
end

Onde salvar esses arquivos com os filtros? Eu gosto de criar uma pasta em app/controllers/filters e colocar os filtros ali. Para facilitar as coisas, adicione essa nova pasta ao autoload do Rails:

module YourApp
  class Application < Rails::Application
    config.autoload_paths << "#{Rails.root}/app/controllers/filters"
  end
end

Espero que com isso você possa organizar um pouco melhor seus controllers, além de poder testar melhor os filtros.

6 Comentários

Deixe um comentário
  1. Diego Alvarez Nogueira / nov 1 2010 3:27 pm

    Parabéns Cássio, muito bom mesmo!
    Espero colocar em prática em breve…

    Abraços

  2. Gostei da solução com a classe de filtro. É interessante a flexibilidade de poder passar como parâmetro de before_filter tanto uma classe ou uma instância de uma classe.

    Na classe base de filtro, eu somente faria uma modificação. Ao invés das classes filhas sobrescreverem o método filter, eu criaria um outro método protegido que é chamado dentro do método filter. Dessa forma, as classes filhas sobrescrevem esse novo método e não mexem na interface pública.

    require 'forwardable'
    class MyBaseFilter
      extend Forwardable
    
      def_delegators :@controller, :redirect_to, :render, :params, :flash
    
      def filter(controller)
        @controller = controller
        filter_logic
      end
    
      protected
      def filter_logic
    
      end
    end
    
    class MyAwesomeFilter < MyBaseFilter
      def filter_logic
        # sua lógica de filtro
      end
    end
    
    • cassiomarques / nov 3 2010 1:39 pm

      Fernando,

      Bacana sua idéia. Eu só não tenho certeza se fazer com que as classes filhas sobrescrevam um método não público seria uma boa. Em geral prefiro manter minhas classes como “caixas pretas” (o que aconteceria caso a classe base fizesse parte de uma gem, por exemplo).

      Obrigado pelo comentário!

  3. Vinicius Baggio Fuentes / nov 3 2010 6:28 pm

    Excelente artigo, Cássio! Eu faço coisas muito parecida com models, extrair comportamento comum, validações e tudo o mais. Essa aplicação com controllers ficou bastante prática e já percebo alguns lugares que poderia aplicar.

    Obrigado por compartilhar!

  4. Entendi.
    Então tudo bem, a gente deixa o método filter_logic público. (risos)

  5. Uchoa / nov 10 2010 3:35 pm

    Cara, massa! Gostei, mesmo.
    Gosto do encadeamento de raciocínio, o desenvolvimento passa-a-passo direto.
    Parabéns!

    E eu tenho alguns controllers que merecem um refactor desses.

    Abraços

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: