Você fica irritado toda vez que vê variáveis de instância mutantes no controlador rails para filtrar dados? Este artigo é para si. 🙂
Filtros
Provavelmente já viu isto antes:
# app/controllers/api/v1/things_controller.rb
módulo API
módulo V1
classe ThingsController < BaseController
def index
@coisas = Coisa.tudo
@coisas = @coisas.where(tamanho: params[:tamanho]) if params[:tamanho]
@coisas = @coisas.where('nome ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
renderizar json: @coisas
fim
fim
fim
fim
Porque é que considero que é um mau código? Porque simplesmente engorda o nosso controlador. Na minha opinião, devemos extrair o máximo de lógica possível dos controladores e utilizar um utilitários ou serviços. Neste caso, vamos implementar um filtro genérico que será capaz de para utilizar em muitos controladores.
Mas espere, primeiro vamos analisar o código atual. Pode ser mau mas funciona. Temos um âmbito inicial (Coisa.tudo) e depois limitam-no se o utilizador tiver passado parâmetro relacionado. Para cada filtro, verificamos efetivamente se o parâmetro foi passado e, em caso afirmativo, se o foi, aplicamos um filtro. A segunda coisa é que não precisamos de utilizar o ivar, podemos utilizar as velhas e simples variáveis locais.
Ok, então. Não poderíamos usar algum objeto de serviço para alterar o âmbito inicial? A execução pode ter o seguinte aspeto:
# app/controllers/api/v1/things_controller.rb
módulo API
módulo V1
classe ThingsController < BaseController
def index
escopo = Thing.all
things = Things::IndexFilter.new.call(scope, params)
renderizar json: things
fim
fim
fim
fim
O aspeto é muito melhor agora, mas é claro que ainda temos de implementar o filtro. Note-se que a assinatura da chamada será a mesma para todos os recursos, pelo que podemos ter uma classe genérica para esta tarefa.
# app/services/generic/index_filter.rb
módulo Generic
classe IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
fim
def call(scope, params)
apply_filters!(self.class.filters.keys, scope, params)
end
private
def apply_filters!(chaves_de_filtro, âmbito, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
apply_filter!(chave_do_filtro, âmbito_actual, params)
end
fim
def apply_filter!(filter_key, scope, params)
filtro = fetch_filter(chave_do_filtro)
return scope unless apply_filter?(filter, params)
filter[:apply].call(scope, params)
fim
def apply_filter?(filter, params)
filter[:apply?].call(params)
fim
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'unknown filter' }
end
end
end
Parece complicado? Nem por isso - toda a magia acontece em #apply_filters!. Pegamos no duplicado do âmbito inicial e aplicamos-lhe cada filtro.
Quando aplicamos o âmbito, isso significa que alteramos o duplicado do nosso âmbito inicial. E esperamos que os filtros sejam implementados como um hash no self.filters método de uma classe infantil. Vamos a isso.
# app/services/things/index_filter.rb
módulo Things
classe IndexFilter (params) {
params[:tamanho].is_a?(String)
},
apply: ->(scope, params) {
scope.where(size: params[:size])
}
}.freeze,
name_contains_filter: {
apply?: ->(params) {
params[:name_contains].is_a?(String)
},
apply: ->(scope, params) {
scope.where('name ILIKE ?', "%#{params[:name_contains]}%")
}
}.freeze
}.congelar
def self.filters
FILTROS
fim
fim
fim
É isso mesmo! Escrevemos mais código, mas os filtros simples terão o mesmo aspeto para todos os recursos. Limpámos o controlador do código responsável de filtragem e forneceu uma classe "especializada" para este efeito que segue muito convenção clara.