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.
Written by Ben Simpson
Related protips
1 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)