Kas sa oled vihane iga kord, kui näed, et rails-kontrolleris muutuvad instantsmuutujad andmete filtreerimiseks? See artikkel on mõeldud teile 🙂 .
Filtrid
Ilmselt olete seda juba varem näinud:
# app/controllers/api/v1/things_controller.rb
moodul API
moodul 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
Miks ma pean seda halvaks kood? Sest see teeb meie kontrolleri lihtsalt paksuks. IMHO peaksime ekstraheerima võimalikult palju loogikat kontrolleritest ja kasutama eesmärgipäraseid abivahendid või teenused. Sellisel juhul rakendame üldise filtri, mida me saame kasutada paljude kontrollerite puhul.
Kuid oodake, analüüsime kõigepealt praegust koodi. See võib olla halb, kuid töötab siiski. Meil on mõningane esialgne reguleerimisala (Thing.all) ja siis piiravad seda, kui kasutaja on läbinud seotud parameeter. Iga filtri puhul me tegelikult kontrollime, kas parameeter anti üle ja kui see on nii, rakendame filtrit. Teine asi on see, et me ei pea kasutama ivar, me võime kasutada tavalised vanad kohalikud muutujad.
Okei, siis. Kas me ei võiks kasutada mõnda teenuse objekti, et muuta esialgset ulatust? Täitmine võib välja näha nii:
# app/controllers/api/v1/things_controller.rb
moodul API
moodul V1
class ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json: things
end
end
end
end
See näeb nüüd palju parem välja, kuid muidugi peame veel filtrit rakendama. Pange tähele, et kõne allkiri on kõikide ressursside puhul sama, nii et meil võib olla mingi üldine klass selle ülesande jaoks.
# app/services/generic/index_filter.rb
moodul Generic
klass 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 |current_scope, filter_key|
apply_filter!(filter_key, current_scope, params)
end
end
def apply_filter!(filter_key, scope, params)
filter = fetch_filter(filter_key)
return scope, välja arvatud juhul, kui 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, 'unknown filter' }
end
end
end
Tundub keeruline? Tegelikult mitte - kogu maagia toimub #apply_filters!. Võtame esialgse ulatuse duplikaadi ja rakendame sellele iga filtri.
Kui me kohaldame ulatust, tähendab see, et me muudame oma esialgse ulatuse duplikaadi. Ja me eeldame, et filtrid on rakendatud hash'ina, mis asub self.filters meetod lapse klassi. Teeme seda.
# app/services/things/index_filter.rb
moodul Things
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
See on kõik! Me oleme kirjutanud rohkem koodi, kuid lihtsad filtrid näevad välja samamoodi viis kõigi ressursside jaoks. Me oleme puhastanud kontrolleri koodist, mis vastutab filtreerimise ja ette nähtud "spetsialiseeritud" klassi selleks, et järgib väga selge konventsioon.