Siete arrabbiati ogni volta che vedete mutare le variabili di istanza nel controller di rails per filtrare i dati? Questo articolo è per voi. 🙂
Filtri
Probabilmente l'avete già visto:
# app/controller/api/v1/things_controller.rb
modulo API
modulo V1
classe ThingsController < BaseController
def index
@cose = 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: @cose
fine
fine
fine
fine
Perché lo considero un cattivo codice? Perché fa semplicemente ingrassare il nostro controllore. IMHO dovremmo estrarre la maggior parte della logica possibile dai controllori e utilizzare un sistema di o servizi. In questo caso implementeremo un filtro generico che sarà in grado di da utilizzare su più controllori.
Ma prima analizziamo il codice attuale. Può essere brutto ma funziona. Abbiamo un ambito iniziale (Cosa.tutti) e poi lo limitano se l'utente ha superato relativo a un parametro. Per ogni filtro controlliamo se il parametro è stato passato e, in caso affermativo, se è stato passato, applichiamo un filtro. La seconda cosa è che non abbiamo bisogno di usare ivar, possiamo usare le vecchie variabili locali.
Ok, allora. Non si potrebbe usare un oggetto di servizio per mutare l'ambito iniziale? L'esecuzione può essere simile a questa:
# app/controller/api/v1/things_controller.rb
modulo API
modulo V1
classe ThingsController < BaseController
def index
ambito = Thing.all
things = Things::IndexFilter.new.call(scope, params)
rende json: things
fine
fine
fine
fine
Ora l'aspetto è molto migliore, ma ovviamente dobbiamo ancora implementare il filtro. Si noti che la firma della chiamata sarà la stessa per tutte le risorse, quindi si può avere una classe generica per questo compito.
# app/services/generic/index_filter.rb
modulo Generic
classe IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
VUOTO_HASH
fine
def call(scope, params)
apply_filters!(self.class.filters.keys, scope, params)
fine
privato
def apply_filters!(filter_keys, scope, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
apply_filter!(filter_key, current_scope, params)
fine
fine
def apply_filter!(filter_key, scope, params)
filtro = fetch_filtro(chiave_filtro)
restituisce l'ambito a meno che apply_filter?(filter, params)
filter[:apply].call(scope, params)
fine
def apply_filter?(filter, params)
filtro[:apply?].call(params)
fine
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'unknown filter' }
end
fine
fine
Sembra complicato? Non proprio: tutta la magia avviene in #apply_filters!. Prendiamo il duplicato dell'ambito iniziale e vi applichiamo ogni filtro.
Quando applichiamo l'ambito, significa che mutiamo il duplicato dell'ambito iniziale. E ci aspettiamo che i filtri siano implementati come un hash nel file self.filters metodo di una classe figlio. Facciamolo.
# app/services/things/index_filter.rb
modulo Things
classe IndexFilter (params) {
params[:size].is_a?(String)
},
applicare: ->(scope, params) {
scope.where(size: params[:size]))
}
}.freeze,
nome_contiene_filtro: {
apply?: ->(params) {
params[:name_contains].is_a?(String)
},
applicare: ->(scope, params) {
scope.where('name ILIKE ?', "%#{params[:name_contains]}%")
}
}.freeze
}.freeze
def self.filters
FILTRI
fine
fine
fine
Ecco fatto! Abbiamo scritto più codice, ma i filtri semplici avranno lo stesso aspetto per tutte le risorse. Abbiamo ripulito il controllore dal codice responsabile di filtraggio e ha fornito una classe "specializzata" per questo scopo che segue molto convenzione chiara.