Czy jesteś zły za każdym razem, gdy widzisz mutowanie zmiennych instancji w kontrolerze rails w celu filtrowania danych? Ten artykuł jest dla Ciebie 🙂
Filtry
Prawdopodobnie widziałeś to już wcześniej:
# app/controllers/api/v1/things_controller.rb
moduł API
moduł V1
class 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
koniec
Dlaczego uważam to za złe kod? Ponieważ po prostu sprawia, że nasz kontroler jest gruby. IMHO powinniśmy wyodrębnić jak najwięcej logiki z kontrolerów i użyć powiązanego z celem narzędzi lub usług. W tym przypadku zaimplementujemy ogólny filtr, który będziemy w stanie do wykorzystania na wielu kontrolerach.
Ale poczekajmy, najpierw przeanalizujmy obecny kod. Może być zły, ale działa. Mamy pewien początkowy zakres (Thing.all), a następnie ograniczają go, jeśli użytkownik przeszedł powiązany parametr. Dla każdego filtra sprawdzamy, czy parametr został przekazany, a jeśli tak, to czy został przekazany, stosujemy filtr. Drugą rzeczą jest to, że nie musimy używać ivar, możemy użyć zwykłe stare zmienne lokalne.
Ok, w takim razie. Czy nie moglibyśmy użyć jakiegoś obiektu usługi do zmutowania początkowego zakresu? Wykonanie może wyglądać następująco:
# app/controllers/api/v1/things_controller.rb
moduł API
moduł V1
class ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
renderowanie json: things
end
end
end
koniec
Teraz wygląda to znacznie lepiej, ale oczywiście musimy jeszcze zaimplementować filtr. Należy pamiętać, że sygnatura wywołania będzie taka sama dla wszystkich zasobów, więc możemy mieć jakąś ogólną klasę do tego zadania.
# app/services/generic/index_filter.rb
moduł Generic
class 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)
end
end
def apply_filter!(filter_key, scope, params)
filter = fetch_filter(filter_key)
return scope unless apply_filter?(filter, params)
filter[:apply].call(scope, 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, 'nieznany filtr' }
end
end
end
Wydaje się skomplikowane? Nie do końca - cała magia dzieje się w #apply_filters!. Bierzemy duplikat początkowego zakresu i stosujemy do niego każdy filtr.
Kiedy stosujemy zakres, oznacza to, że mutujemy duplikat naszego początkowego zakresu. Oczekujemy, że filtry zostaną zaimplementowane jako hash w self.filters metoda klasy dziecka. Zróbmy to.
# app/services/things/index_filter.rb
moduł 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
end
end
end
To wszystko! Napisaliśmy więcej kodu, ale proste filtry będą wyglądać tak samo dla wszystkich zasobów. Wyczyściliśmy kontroler z kodu odpowiedzialnego filtrowania i udostępnił w tym celu "wyspecjalizowaną" klasę, która jest bardzo jasna konwencja.