Last Updated: December 31, 2020
·
1.428K
· bsimpson

Query + Proxy Objects for Sweet Data Access

I've become interested in applying the patterns established by the Gang of Four in my code whenever I can. This helps me understand what works well, and what doesn't.

While refactoring some messy data access in a Rails controller action, I stumbled across an interesting combination of two different patterns - the query object pattern with the proxy object pattern.

Query Object

I set out to implement the query pattern by creating a class that takes a Consumer instance, and uses it to prefix to Voucher to constrain its results by its associated foreign key consumer_id:

class MyVouchers

  attr_accessor :consumer

  def initialize(consumer)
    self.consumer = consumer
  end

  def for_display
    self.consumer.vouchers.joins(:partners)
    .where(["partners.business_name != ?)", Partner::LEGO])
  end
end

This meant instead of embedding lots of ActiveRecord model lookups and scope chaining in the controller, I could now run the following and return the same result set:

MyVouchers.new(current_consumer).for_display.active

Proxy Object

This was good, but a wordy public interface. I didn't feel the method call to_display provided much value, so I wanted to eliminate it from the method calls. After some experimentation, I implemented the proxy pattern, which makes use of method_missing to delegate its calls to another object. In this case, that target was the Voucher class. But with the scoping from for_display applied. I modified my class to look like this:

class MyVouchers

  attr_accessor :consumer

  def initialize(consumer)
    self.consumer = consumer
  end

  def for_display
    self.consumer.vouchers.joins(:partners)
    .where(["partners.business_name != ?)", Partner::LEGO])
  end

  private

  def method_missing(method, *args, &block)
    for_display.send(method, *args, &block)
  end

Now in the controller, I can eliminate to_display and call MyVouchers.new(current_consumer).active. The method active still lives as a scope definition within the Voucher class so it can be reused outside my query object. And less domain knowledge is required to invoke my query object since instantiation is sufficient to begin chaining other scopes.

I was pleased to see that outside of using the basic patterns in isolation, they can be combined in interesting ways that leverage the strengths of both patterns.

1 Response
Add your response

What is a reason to make consumer public accessed to write and read?

method_missing is not so good practice. If you don't want extra methods then don't use class and instance.

module MyVouchers
  def for_display consumer
    self.consumer.vouchers.joins(:partners)
    .where(["partners.business_name != ?)", Partner::LEGO])
  end

Usage:

MyVouchers.for_display(consumer)
over 1 year ago ·