Jste naštvaní pokaždé, když vidíte, že v rails controlleru dochází k mutaci instančních proměnných za účelem filtrování dat? Tento článek je pro vás. 🙂
Filtry
Pravděpodobně jste to už někdy viděli:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
třída 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
Proč považuji za špatný kód? Protože náš regulátor jednoduše tloustne. IMHO bychom měli z řadičů vytáhnout co nejvíce logiky a použít účelově vázaný systém. nástroje nebo služby. V tomto případě budeme implementovat obecný filtr, který budeme schopni pro použití v mnoha řadičích.
Ale počkejte, nejprve analyzujme současný kód. Může být špatný, ale funguje. Máme určitý počáteční rozsah (Thing.all) a pak je omezuje, pokud uživatel prošel související parametr. U každého filtru skutečně zkontrolujeme, zda byl param předán, a pokud ano, tak jej předáme, použijeme filtr. Druhá věc je, že nemusíme používat ivar, můžeme použít staré lokální proměnné.
Dobře tedy. Nemohli bychom použít nějaký objekt služby, který by mutoval počáteční rozsah? Provedení může vypadat takto:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
třída ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json: things
end
end
end
end
Nyní to vypadá mnohem lépe, ale samozřejmě musíme ještě implementovat filtr. Všimněte si, že signatura volání bude stejná pro všechny prostředky, takže můžeme mít. nějakou obecnou třídu pro tento úkol.
# app/services/generic/index_filter.rb
modul Generic
třída 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)
konec
konec
def apply_filter!(filter_key, scope, params)
filter = fetch_filter(filter_key)
return scope unless apply_filter?(filter, params)
filter[:apply].call(scope, params)
konec
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
Zdá se vám to složité? Ne tak docela - všechna kouzla se odehrávají v #apply_filters!. Vezmeme duplikát původního oboru a použijeme na něj jednotlivé filtry.
Když použijeme obor, znamená to, že zmutujeme duplikát našeho původního oboru. Očekáváme, že filtry budou implementovány jako hash v souboru self.filters metoda třídy potomků. Udělejme to.
# app/services/things/index_filter.rb
modul 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
FILTRY
konec
konec
end
A je to! Napsali jsme více kódu, ale jednoduché filtry budou vypadat stejně. cesta pro všechny zdroje. Vyčistili jsme kontrolér od kódu zodpovědného za filtrování a poskytl "specializovanou" třídu pro tento účel, která se řídí velmi jasná konvence.