미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
Are you angry every time you see mutating instance variables in rails controller to filter data? This article is for you. 🙂
You have probably seen this before:
# app/controllers/api/v1/things_controller.rb
module API
module 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
end
Why do I consider it to be a bad 코드? Because it simply makes our controller fat.
IMHO we should extract as many logic as we can from controllers and use a purpose-related
utils or services. In this case we will implement a generic filter that we will be able
to use across many controllers.
But wait, first let’s analyse the current code. It can be bad but works, though.
We have some initial scope (Thing.all
) and then are limiting it if user has passed
related parameter. For each filter we actually check if the param was passed and if so,
we apply a filter. The second thing is that we don’t need to use the ivar, we can use
plain old local variables.
Ok, then. Couldn’t we use some service object to mutate the initial scope?
The execution can look like this:
# app/controllers/api/v1/things_controller.rb
module API
module V1
class ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json: things
end
end
end
end
It looks much better now, but of course we have to implement the filter yet.
Note that call’s signature will be the same for all resources, so we can have
some generic class for this task.
# app/services/generic/index_filter.rb
module 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, 'unknown filter' }
end
end
end
Seems complicated? Not really – all the magic happens in #apply_filters!
.
We take the duplicate of the initial scope and apply each filter to it.
When we apply the scope it means we mutate the duplicate of our initial scope.
And we expect filters to be implemented as a hash in the self.filters
method
of a child class. Let’s do it.
# app/services/things/index_filter.rb
module Things
class IndexFilter < Generic::IndexFilter
FILTERS = {
size_filter: {
apply?: ->(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
FILTERS
end
end
end
That’s it! We have written more code, but the simple filters will look the same
way for all the resources. We have cleaned controller from the code responsible
of filtering and provided ‘specialised’ class for this purpose that follows very
clear convention.
자세히 읽어보세요: