Είστε θυμωμένοι κάθε φορά που βλέπετε μεταβλητές μεταβλητών instance στον ελεγκτή rails για να φιλτράρετε δεδομένα; Αυτό το άρθρο είναι για εσάς. 🙂
Φίλτρα
Πιθανόν να το έχετε ξαναδεί αυτό:
# app/controllers/api/v1/things_controller.rb
ενότητα API
ενότητα 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
Γιατί το θεωρώ κακό κωδικός? Επειδή απλά παχαίνει τον ελεγκτή μας. IMHO θα πρέπει να εξάγουμε όσο το δυνατόν περισσότερη λογική από τους ελεγκτές και να χρησιμοποιήσουμε ένα σχετικό με το σκοπό βοηθήματα ή υπηρεσίες. Σε αυτή την περίπτωση θα υλοποιήσουμε ένα γενικό φίλτρο που θα μπορούμε να για χρήση σε πολλούς ελεγκτές.
Αλλά περιμένετε, ας αναλύσουμε πρώτα τον τρέχοντα κώδικα. Μπορεί να είναι κακός, αλλά λειτουργεί. Έχουμε κάποιο αρχικό πεδίο εφαρμογής (Thing.all) και στη συνέχεια το περιορίζουν εάν ο χρήστης έχει περάσει το σχετική παράμετρος. Για κάθε φίλτρο ελέγχουμε αν η παράμετρος έχει περάσει και αν ναι, εφαρμόζουμε ένα φίλτρο. Το δεύτερο πράγμα είναι ότι δεν χρειάζεται να χρησιμοποιήσουμε το ivar, μπορούμε να χρησιμοποιήσουμε το απλές τοπικές μεταβλητές.
Εντάξει, τότε. Δεν θα μπορούσαμε να χρησιμοποιήσουμε κάποιο αντικείμενο υπηρεσίας για να μεταβάλλουμε το αρχικό πεδίο εφαρμογής; Η εκτέλεση μπορεί να έχει ως εξής:
# app/controllers/api/v1/things_controller.rb
ενότητα API
ενότητα V1
class ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json: things
end
end
end
end
Φαίνεται πολύ καλύτερα τώρα, αλλά φυσικά πρέπει να εφαρμόσουμε το φίλτρο ακόμα. Σημειώστε ότι η υπογραφή της κλήσης θα είναι η ίδια για όλους τους πόρους, οπότε μπορούμε να έχουμε κάποια γενική κλάση για την εργασία αυτή.
# app/services/generic/index_filter.rb
ενότητα 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 εκτός αν 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
Φαίνεται περίπλοκο; Όχι πραγματικά - όλη η μαγεία συμβαίνει στο #apply_filters!. Παίρνουμε το αντίγραφο του αρχικού πεδίου εφαρμογής και εφαρμόζουμε κάθε φίλτρο σε αυτό.
Όταν εφαρμόζουμε την εμβέλεια σημαίνει ότι μεταλλάσσουμε το αντίγραφο της αρχικής μας εμβέλειας. Και περιμένουμε τα φίλτρα να υλοποιηθούν ως hash στο self.filters μέθοδος μιας παιδικής κλάσης. Ας το κάνουμε.
# app/services/things/index_filter.rb
module 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
FILTERS
end
end
end
Αυτό είναι! Έχουμε γράψει περισσότερο κώδικα, αλλά τα απλά φίλτρα θα μοιάζουν με τα ίδια τρόπος για όλους τους πόρους. Έχουμε καθαρίσει τον ελεγκτή από τον υπεύθυνο κώδικα του φιλτραρίσματος και παρέχεται "εξειδικευμένη" κλάση για το σκοπό αυτό που ακολουθεί πολύ σαφής σύμβαση.