Te enfadas cada vez que ves mutar variables de instancia en el controlador rails para filtrar datos? Este artículo es para ti 🙂 .
Filtros
Probablemente ya lo haya visto antes:
# app/controllers/api/v1/things_controller.rb
módulo API
módulo V1
clase ThingsController < BaseController
def index
@cosas = Cosa.todo
@cosas = @cosas.where(tamaño: parámetros[:tamaño]) if parámetros[:tamaño]
@things = @things.where('nombre ILIKE ?', "%#{params[:nombre_contiene]}%") if params[:nombre_contiene]
render json: @cosas
end
end
end
fin
¿Por qué lo considero un mal código? Porque simplemente engorda a nuestro controlador. En mi humilde opinión, deberíamos extraer toda la lógica que podamos de los controladores y utilizar un propósito relacionado con utilidades o servicios. En este caso implementaremos un filtro genérico que podremos para utilizar en muchos controladores.
Pero espera, primero analicemos el código actual. Puede ser malo pero funciona. Tenemos un alcance inicial (Cosa.todo) y luego se limitan si el usuario ha pasado relacionado. Para cada filtro comprobamos si se ha pasado el parámetro y si es así, aplicamos un filtro. Lo segundo es que no necesitamos usar el ivar, podemos usar simples variables locales.
De acuerdo. ¿No podríamos usar algún objeto de servicio para mutar el ámbito inicial? La ejecución puede ser así:
# app/controllers/api/v1/things_controller.rb
módulo API
módulo V1
clase ThingsController < BaseController
def índice
ámbito = Thing.all
things = Things::IndexFilter.new.call(ámbito, parámetros)
render json: cosas
end
end
end
fin
Ahora se ve mucho mejor, pero por supuesto tenemos que implementar el filtro todavía. Tenga en cuenta que la firma de la llamada será la misma para todos los recursos, por lo que podemos tener alguna clase genérica para esta tarea.
# app/services/generic/index_filter.rb
módulo Generic
clase IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
end
def call(ámbito, parámetros)
apply_filters!(self.class.filters.keys, scope, params)
end
privado
def ¡aplicar_filtros!(claves_filtro, ámbito, parámetros)
filter_keys.inject(ámbito.dup) do |ámbito_actual, filter_key|
apply_filter!(clave_filtro, ámbito_actual, parámetros)
end
fin
def aplicar_filtro!(clave_filtro, ámbito, parámetros)
filter = fetch_filter(clave_filtro)
return ámbito unless aplicar_filtro?(filtro, parámetros)
filter[:apply].call(ámbito, parámetros)
fin
def ¿aplicar_filtro?(filtro, parámetros)
filter[:apply?].call(params)
end
def buscar_filtro(clave_filtro)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'filtro desconocido' }
end
end
fin
¿Parece complicado? En realidad no: toda la magia ocurre en ¡#apply_filters!. Tomamos el duplicado del ámbito inicial y le aplicamos cada filtro.
Cuando aplicamos el ámbito significa que mutamos el duplicado de nuestro ámbito inicial. Y esperamos que los filtros se implementen como un hash en el archivo auto.filtros método de una clase infantil. Hagámoslo.
Ya está. Hemos escrito más código, pero los filtros simples tendrán el mismo aspecto para todos los recursos. Hemos limpiado controlador del código responsable de filtrado y proporcionó una clase "especializada" para este fin que sigue muy convención clara.