Är du arg varje gång du ser muterande instansvariabler i Rails Controller för att filtrera data? Den här artikeln är för dig.
Filter
Du har säkert sett det här förut:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
klass ThingsController < BasController
def index
@things = Ting.alla
@things = @things.where(size: params[:size]) if params[:size]
@things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
render json: @ting
slut
slut
slut
slut
Varför anser jag att det är en dålig kod? För att det helt enkelt gör vår controller fet. IMHO bör vi extrahera så mycket logik som möjligt från styrenheter och använda en ändamålsrelaterad verktyg eller tjänster. I det här fallet kommer vi att implementera ett generiskt filter som vi kommer att kunna att använda över många styrenheter.
Men vänta, låt oss först analysera den nuvarande koden. Den kan vara dålig men fungerar ändå. Vi har en viss initial omfattning (Sak.alla) och sedan begränsar det om användaren har passerat relaterad parameter. För varje filter kontrollerar vi faktiskt om parametern godkändes och i så fall, vi applicerar ett filter. Den andra saken är att vi inte behöver använda ivar, vi kan använda vanliga gamla lokala variabler.
Okej, då så. Kan vi inte använda något serviceobjekt för att mutera det initiala omfånget? Utförandet kan se ut så här:
# app/controllers/api/v1/things_controller.rb
modul API
modul V1
klass ThingsController < BasController
def index
scope = Ting.alla
saker = Things::IndexFilter.new.call(scope, params)
rendera json: saker
slut
slut
slut
slut
Det ser mycket bättre ut nu, men vi måste naturligtvis implementera filtret ännu. Observera att anropets signatur kommer att vara densamma för alla resurser, så vi kan ha någon generisk klass för denna uppgift.
# app/services/generic/index_filter.rb
modul Generisk
klass IndexFilter
EMPTY_HASH = {}.freeze
def self.filter
EMPTY_HASH
slut
def anrop(omfattning, params)
apply_filters!(self.class.filters.keys, scope, params)
slut
privat
def tillämpa_filter!(filter_nycklar, omfattning, params)
filter_keys.inject(scope.dup) do |aktuellt_scope, filter_nyckel|
apply_filter!(filter_key, aktuellt_område, params)
slut
slut
def apply_filter!(filter_key, scope, params)
filter = hämta_filter(filter_nyckel)
return scope om inte apply_filter?(filter, params)
filter[:apply].call(scope, params)
slut
def apply_filter?(filter, params)
filter[:apply?].call(params)
slut
def hämta_filter(filter_nyckel)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'okänt filter' }
end
slut
slut
Verkar det komplicerat? Inte riktigt - all magi sker i #apply_filter!. Vi tar duplikatet av det ursprungliga omfånget och tillämpar varje filter på det.
När vi tillämpar omfattningen innebär det att vi muterar duplikatet av vår ursprungliga omfattning. Och vi förväntar oss att filter ska implementeras som en hash i self.filter metod av en barnklass. Låt oss göra det.
# app/services/things/index_filter.rb
modul Saker
klass IndexFilter (params) {
params[:size].is_a?(String)
},
apply: ->(scope, params) {
scope.where(size: params[:size])
}
}.freeze,
namn_innehåller_filter: {
apply?: ->(params) {
params[:name_contains].is_a?(String)
},
apply: ->(scope, params) {
scope.where('namn LIKNAR ?',"%#{params[:namn_innehåller]}%")
}
}.frysa
}.freeze
def self.filter
FILTRAR
slut
slut
slut
Nu är det klart! Vi har skrivit mer kod, men de enkla filtren kommer att se likadana ut sätt för alla resurser. Vi har rensat kontrollern från den ansvariga koden av filtrering och tillhandahållit en "specialiserad" klass för detta ändamål som följer mycket tydlig konvention.