Bliver du vred, hver gang du ser muterende instansvariabler i rails controller for at filtrere data? Så er denne artikel noget for dig.
Filtre
Du har sikkert set det før:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
klasse ThingsController < BaseController
def index
@things = Ting.alle
@things = @things.where(size: params[:size]) if params[:size]
@things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
render json: @things
slut
slut
slut
slut
Hvorfor mener jeg, at det er en dårlig Kode? Fordi det simpelthen gør vores controller fed. IMHO bør vi udtrække så meget logik som muligt fra controllere og bruge en formålsrelateret værktøjer eller tjenester. I dette tilfælde vil vi implementere et generisk filter, som vi vil kunne at bruge på tværs af mange controllere.
Men vent, lad os først analysere den nuværende kode. Den kan være dårlig, men fungerer dog. Vi har nogle indledende muligheder (Ting.alle) og begrænser den derefter, hvis brugeren har bestået relateret parameter. For hvert filter tjekker vi faktisk, om parameteren blev sendt, og i så fald, vi anvender et filter. Den anden ting er, at vi ikke behøver at bruge ivar, vi kan bruge almindelige gamle lokale variabler.
Okay, så. Kan vi ikke bruge et serviceobjekt til at mutere det oprindelige scope? Udførelsen kan se sådan ud:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
klasse ThingsController < BaseController
def index
scope = Ting.alle
ting = Things::IndexFilter.new.call(scope, params)
render json: ting
end
slut
slut
slut
Det ser meget bedre ud nu, men vi mangler selvfølgelig at implementere filteret. Bemærk, at kaldets signatur vil være den samme for alle ressourcer, så vi kan have en generisk klasse til denne opgave.
# 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
privat
def apply_filters!(filter_keys, scope, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
apply_filter!(filter_key, current_scope, params)
slut
slut
def apply_filter!(filter_key, scope, params)
filter = hent_filter(filter_key)
return scope unless apply_filter?(filter, params)
filter[:apply].call(scope, params)
slut
def apply_filter?(filter, params)
filter[:apply?].call(params)
end
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'ukendt filter' }
end
end
slut
Virker det kompliceret? Egentlig ikke - al magien sker i #apply_filters!. Vi tager duplikatet af det oprindelige scope og anvender hvert filter på det.
Når vi anvender scopet, betyder det, at vi muterer duplikatet af vores oprindelige scope. Og vi forventer, at filtre implementeres som en hash i self.filters metode af en børneklasse. Lad os gø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
slut
Så er det nok! Vi har skrevet mere kode, men de enkle filtre vil se ud på samme måde måde for alle ressourcerne. Vi har renset controlleren fra den ansvarlige kode af filtrering og leverede en 'specialiseret' klasse til dette formål, der følger meget klar konvention.