Design pattern: Query objects
- Avoid "fat model"
- Easy to test
- Extenable
- Single response sibility
# app/models/booking.rb belongs_to :beneficiary scope :by_price, -> (direction) { order(price: direction) } scope :by_booking_date, -> (direction) { order(booking_date: direction) }
Without scope:
# app/queries/booking_query.rb class BookingQuery class << self SORT_OPTIONS = %w[by_price by_booking_date by_beneficiary_savings] def call(params, scope = Booking.all) scope = scope.where(conditions) scope = by_status(scope, params[:status]) scope = by_active_beneficiary(scope, params[:active_beneficiary]) scope = sort_by(scope, params[:sort], params[:direction]) scope end # Make the class extendable def conditions; end def by_active_beneficiary(scope, value) return scope if value.blank? scope.where(beneficiaries: { active: value }) end def sort_by(scope, sort, direction) sort = sort.presence_in(SORT_OPTIONS) || :by_booking_date direction = direction == :desc ? :desc : :asc scope.public_send(sort, direction) end end end
With scope
# app/queries/scopeable.rb module Scopeable def scope(name, body) define_method name do |*args, **kwargs| relation = instance_exec(*args, **kwargs, &body) relation || self end end end
# app/queries/booking_query.rb class BookingQuery module Scopes extend Scopeable # Some scopes that only use in this Query Object scope :by_status, -> (status) { status && where(status: status) } def by_beneficiary_savings(direction) includes(:beneficiary).order("beneficiaries.personal_savings #{direction}") end end class << self SORT_OPTIONS = %w[by_price by_booking_date by_beneficiary_savings] def call(params, scope = Booking.all) sort = sort.presence_in(SORT_OPTIONS) || :by_booking_date direction = direction == :desc ? :desc : :asc scope.extending(Scopes) .where(conditions) .by_status(params[:status]) .by_beneficiary_savings(direction) .public_send(sort, direction) end # Make the class extendable def conditions; end end end
Extend
# app/queries/high_price_booking_query.rb class HighPriceBookingQuery < BookingQuery class << self def conditions 'price > 1000000' end end end
Usage
# Controller filters = {status: :confirmed, active_beneficiary: true } HighPriceBookingQuery.call(filters.merge(sort_params)) def sort_params # { sort: :by_price, direction: :desc } params.slice(:sort, :direction) end