Oletko vihainen joka kerta, kun näet mutating instanssi muuttujat rails ohjain suodattaa tietoja? Tämä artikkeli on sinua varten. 🙂
Suodattimet
Olet luultavasti nähnyt tämän ennenkin:
# app/controllers/api/v1/things_controller.rb
moduuli API
moduuli 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
Miksi pidän sitä huonona koodi? Koska se yksinkertaisesti lihottaa valvojamme. IMHO meidän pitäisi poimia mahdollisimman paljon logiikkaa ohjaimista ja käyttää tarkoitukseen liittyvää apuvälineitä tai palveluja. Tässä tapauksessa otamme käyttöön yleisen suodattimen, jonka avulla voimme käyttää käytettäväksi useissa ohjaimissa.
Mutta odota, analysoidaan ensin nykyinen koodi. Se voi olla huono mutta toimii kuitenkin. Meillä on jonkin verran alkuperäistä soveltamisalaa (Thing.all) ja sitten rajoitetaan sitä, jos käyttäjä on läpäissyt liittyvä parametri. Jokaisen suodattimen kohdalla tarkistetaan, onko parametrin arvo annettu ja jos on, niin onko se annettu, käytämme suodatinta. Toinen asia on se, että meidän ei tarvitse käyttää ivaria, vaan voimme käyttää komentoa tavalliset paikalliset muuttujat.
Hyvä on sitten. Emmekö voisi käyttää jotakin palveluobjektia alkuperäisen laajuuden muuttamiseen? Suoritus voi näyttää tältä:
# app/controllers/api/v1/things_controller.rb
moduuli API
moduuli V1
class ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json: things
end
end
end
end
Se näyttää nyt paljon paremmalta, mutta meidän on tietysti vielä toteutettava suodatin. Huomaa, että kutsun allekirjoitus on sama kaikille resursseille, joten meillä voi olla seuraavat ominaisuudet jokin yleinen luokka tätä tehtävää varten.
# app/services/generic/index_filter.rb
moduuli Generic
luokka 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 ellei 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
Vaikuttaa monimutkaiselta? Ei oikeastaan - kaikki taika tapahtuu #apply_filters!. Otetaan alkuperäisen laajuuden kaksoiskappale ja sovelletaan siihen kutakin suodatinta.
Kun sovellamme soveltamisalaa, se tarkoittaa, että muunnamme alkuperäisen soveltamisalamme kopion. Ja odotamme, että suodattimet toteutetaan hash-tietueena tiedostossa self.filters menetelmä lapsiluokassa. Tehdään se.
# app/services/things/index_filter.rb
moduuli 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
Juuri noin! Olemme kirjoittaneet enemmän koodia, mutta yksinkertaiset suodattimet näyttävät samalta. tie kaikille resursseille. Olemme puhdistaneet ohjaimen koodista, joka on vastuussa suodatuksen ja tarjotaan "erikoistunut" luokka tähän tarkoitukseen, joka seuraa hyvin selkeä yleissopimus.