Ben je elke keer boos als je mutating instance variables in rails controller ziet om gegevens te filteren? Dit artikel is voor jou. 🙂
Filters
Je hebt dit waarschijnlijk al eerder gezien:
# app/controllers/api/v1/things_controller.rb
module API
module V1
klasse ThingsController < BaseController
def index
@things = Ding.alle
@things = @things.where(size: params[:size]) if params[:size]
@things = @things.where('name ILIKE ?', "%#{params[:naam_bevat]}%") if params[:naam_bevat]
json weergeven: @dingen
einde
einde
einde
einde
Waarom beschouw ik het als een slechte code? Omdat het onze controller gewoon vet maakt. IMHO moeten we zoveel mogelijk logica uit controllers halen en een doelgerelateerde utils of services. In dit geval implementeren we een generiek filter waarmee we te gebruiken op veel controllers.
Maar wacht, laten we eerst de huidige code analyseren. Het kan slecht zijn, maar het werkt wel. We hebben wat initiële reikwijdte (Ding.alle) en dan beperken als de gebruiker geslaagd is voor gerelateerde parameter. Voor elk filter controleren we of de parameter is doorgegeven en zo ja, passen we een filter toe. Het tweede is dat we de ivar niet hoeven te gebruiken, we kunnen gebruik maken van gewone oude lokale variabelen.
Oké, dan. Kunnen we niet een serviceobject gebruiken om de initiële scope te muteren? De uitvoering kan er als volgt uitzien:
# app/controllers/api/v1/things_controller.rb
module API
module V1
klasse ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
json weergeven: things
einde
einde
einde
einde
Het ziet er nu veel beter uit, maar we moeten het filter natuurlijk nog implementeren. Merk op dat de handtekening van de oproep hetzelfde zal zijn voor alle bronnen, dus we kunnen een generieke klasse voor deze taak.
# app/services/generiek/index_filter.rb
module Algemeen
klasse IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
einde
def aanroep(bereik, params)
pas_filters toe!(self.class.filters.keys, bereik, params)
einde
privé
def pas_filters toe!(filter_keys, bereik, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
pas_filter!(filter_key, huidige_scope, params) toe
einde
einde
def apply_filter!(filter_key, scope, params)
filter = fetch_filter(filter_key)
scope terug tenzij apply_filter?(filter, params)
filter[:apply].call(scope, params)
einde
def toepassen_filter?(filter, params)
filter[:toepassen?].aanroepen(params)
einde
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'unknown filter' }
einde
einde
einde
Lijkt het ingewikkeld? Niet echt - alle magie gebeurt in #oe_filters!. We nemen het duplicaat van de initiële scope en passen er elk filter op toe.
Als we de scope toepassen, betekent dit dat we het duplicaat van onze initiële scope muteren. En we verwachten dat filters worden geïmplementeerd als een hash in de zelf.filters methode van een kindklasse. Laten we het doen.
Dat is het! We hebben meer code geschreven, maar de eenvoudige filters zien er hetzelfde uit manier voor alle bronnen. We hebben de controller uit de verantwoordelijke code verwijderd van filteren en leverde hiervoor een 'gespecialiseerde' klasse die zeer duidelijke conventie.