Er du sint hver gang du ser muterende instansevariabler i rails-kontrolleren for å filtrere data? Denne artikkelen er for deg....
Filtre
Du har sikkert sett dette før:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
class ThingsController < BaseController
def index
@things = Thing.all
@things = @things.where(size: params[:size]) if params[:size]
@things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
render json: @things
end
end
end
end
Hvorfor anser jeg det som en dårlig kode? Fordi det rett og slett gjør kontrolleren vår fet. IMHO bør vi trekke ut så mye logikk som mulig fra kontrollerne og bruke en formålsrelatert verktøy eller tjenester. I dette tilfellet vil vi implementere et generisk filter som vi vil kunne til bruk på tvers av mange kontrollere.
Men vent, la oss først analysere den nåværende koden. Den kan være dårlig, men fungerer likevel. Vi har et visst innledende omfang (Ting.alle) og begrenser den deretter hvis brukeren har bestått relatert parameter. For hvert filter sjekker vi faktisk om parameteren ble sendt, og i så fall, bruker vi et filter. Den andre tingen er at vi ikke trenger å bruke ivar, vi kan bruke vanlige, gamle lokale variabler.
Ok, da så. Kan vi ikke bruke et tjenesteobjekt til å mutere det opprinnelige omfanget? Utførelsen kan se slik ut:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
class ThingsController < BaseController
def index
scope = Thing.all
ting = Things::IndexFilter.new.call(scope, params)
render json: ting
end
end
end
end
Det ser mye bedre ut nå, men vi må selvfølgelig implementere filteret ennå. Merk at kallets signatur vil være den samme for alle ressurser, slik at vi kan ha en generisk klasse for denne oppgaven.
# app/services/generic/index_filter.rb
modul Generisk
klasse IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
end
def call(scope, params)
apply_filters!(self.class.filters.keys, scope, params)
end
private
def apply_filters!(filter_keys, scope, params)
filter_keys.inject(scope.dup) do |aktuelt_omfang, filter_nøkkel|
apply_filter!(filter_key, current_scope, params)
end
end
def apply_filter!(filter_key, scope, params)
filter = fetch_filter(filter_key)
return scope med mindre apply_filter?(filter, params)
filter[:apply].call(scope, params)
end
def apply_filter?(filter, params)
filter[:apply?].call(params)
end
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'ukjent filter' }
end
end
end
Virker det komplisert? Ikke egentlig - alt det magiske skjer i #apply_filters!. Vi tar duplikatet av det opprinnelige omfanget og bruker hvert filter på det.
Når vi bruker omfanget, betyr det at vi muterer duplikatet av det opprinnelige omfanget. Og vi forventer at filtre implementeres som en hash i self.filters metode av en barneklasse. La oss gjøre det.
# app/services/things/index_filter.rb
modul Ting
class IndexFilter (params) {
params[:size].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
}.freeze
def self.filters
FILTERS
end
end
end
Nå er det nok! Vi har skrevet mer kode, men de enkle filtrene vil se like ut måte for alle ressursene. Vi har renset kontrolleren fra den ansvarlige koden av filtrering og gitt "spesialisert" klasse for dette formålet som følger veldig klar konvensjon.