Ärgern Sie sich auch jedes Mal, wenn Sie sehen, dass Instanzvariablen im Rails-Controller mutieren, um Daten zu filtern? Dieser Artikel ist für Sie 🙂 .
Filter
Sie haben das wahrscheinlich schon einmal gesehen:
# app/controllers/api/v1/things_controller.rb
Modul API
Modul V1
Klasse ThingsController < BaseController
def index
@things = Thing.all
@things = @things.where(Größe: params[:Größe]) if params[:Größe]
@things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
render json: @dinge
end
end
end
end
Warum halte ich es für eine schlechte Code? Weil es unseren Controller einfach fett macht. IMHO sollten wir so viel Logik wie möglich aus Controllern extrahieren und eine zweckbezogene Dienstprogramme oder Dienste. In diesem Fall werden wir einen generischen Filter implementieren, der es uns ermöglicht für viele Controller zu verwenden.
Aber warten Sie, lassen Sie uns zuerst den aktuellen Code analysieren. Er kann schlecht sein, funktioniert aber trotzdem. Wir haben einen anfänglichen Spielraum (Sache.alle) und schränken es dann ein, wenn der Benutzer die bezogenen Parameter. Für jeden Filter wird geprüft, ob der Parameter übergeben wurde und wenn ja, wenden wir einen Filter an. Die zweite Sache ist, dass wir den ivar nicht verwenden müssen, wir können einfache alte lokale Variablen.
Na gut, dann. Könnten wir nicht ein Dienstobjekt verwenden, um den anfänglichen Bereich zu ändern? Die Ausführung kann wie folgt aussehen:
# app/controllers/api/v1/things_controller.rb
Modul API
Modul V1
Klasse ThingsController < BaseController
def index
Bereich = Thing.all
things = Things::IndexFilter.new.call(scope, params)
json wiedergeben: things
end
end
end
end
Es sieht jetzt viel besser aus, aber natürlich müssen wir den Filter noch implementieren. Beachten Sie, dass die Signatur des Aufrufs für alle Ressourcen gleich sein wird, so dass wir eine generische Klasse für diese Aufgabe.
# app/services/generic/index_filter.rb
Modul Generisch
Klasse IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
end
def call(bereich, params)
apply_filters!(self.class.filters.keys, Bereich, params)
end
privat
def apply_filters!(filter_keys, bereich, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
apply_filter!(filter_key, aktueller_bereich, params)
end
end
def apply_filter!(filter_key, bereich, params)
filter = fetch_filter(filter_key)
Bereich zurückgeben, es sei denn apply_filter?(filter, params)
filter[:apply].call(bereich, 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, 'unbekannter Filter' }
end
end
end
Klingt kompliziert? Nicht wirklich - die ganze Magie geschieht in #apply_filters!. Wir nehmen das Duplikat des ursprünglichen Bereichs und wenden jeden Filter darauf an.
Wenn wir den Bereich anwenden, bedeutet das, dass wir das Duplikat unseres ursprünglichen Bereichs verändern. Und wir erwarten, dass die Filter als Hash in der selbst.Filter Methode einer Kinderklasse. Los geht's.
# app/services/things/index_filter.rb
Modul Things
Klasse IndexFilter (params) {
params[:size].is_a?(String)
},
apply: ->(scope, params) {
scope.where(Größe: params[:Größe])
}
}.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
Das war's! Wir haben mehr Code geschrieben, aber die einfachen Filter werden gleich aussehen Weg für alle Ressourcen. Wir haben Controller aus dem Code verantwortlich gereinigt der Filterung und stellte zu diesem Zweck eine "spezialisierte" Klasse zur Verfügung, die sehr klare Konvention.