Augmenting an Object With the Proxy Pattern
- 3 minutes read - 479 wordsLet’s look at the proxy pattern in Ruby.
Here’s a Ruby class that claps:
class Hands
def clap
"👏"
end
end
And here’s me using an instance of it to do something surprising.
>> 3.times { hands.clap }
"👏"
"👏"
"👏"
=> 3
>> hands.times_called(:clap)
=> 3
times_called
is not defined in Hands
, or any parent of it. So what gives?
Did I leave it off the first code block? What’s going on here?
I recently learned about and had the opportunity to implement a proxy class. In
this post, I’ll share what I learned, showing how hands
gets this special behavior.
Terminology
Let’s start with some terminology. For a deeper dive, check out the excellent Proxy in Ruby on Refactoring Guru.
Proxies use the following terms:
- Service interface
- Service object
- Reference field
- Supporting methods
Proxies have a service interface. Like the ‘I’ in API, the interface defines inputs and outputs of the proxy class.
That interface must match the proxy’s service object, the thing being proxied. If the service object quacks, the proxy must quack.
Proxies store their service object in a reference field. This is how proxies memoize their service object and delegate requests to it.
Another feature of a proxy are supporting methods. These can report information, handle side effects, and more. These methods are the reason for building the proxy, because the service object can’t or shouldn’t implement them itself.
My Ruby Proxy
Here’s my Ruby implementation. I’ll start with the code, followed by a method-by-method breakdown.
class Proxy
def initialize(service_object)
@call_history = []
@service_object = service_object
end
def method_missing(method, *args)
@call_history.push(method)
if @service_object.respond_to?(method)
@service_object.send(method, *args)
else
raise NotImplementedError
end
end
def times_called(method)
@call_history.count { |called| called == method }
end
end
We start with the object instantiation:
class Proxy
def initialize(service_object)
@call_history = []
@service_object = service_object
end
Our argument is the service object, which gets saved as
@service_object
. We also have an ivar @call_history
, that’s read in a
supporting method.
Next, we let the proxy pass any messages it doesn’t recognize, in this case,
every message, to its service object via Ruby’s method_missing
. The proxy
class quacks just like its service object.
def method_missing(method, *args)
@call_history.push(method)
if @service_object.respond_to?(method)
@service_object.send(method, *args)
else
raise NotImplementedError
end
end
Before we move on, notice this line of code:
def method_missing(method, *args)
@call_history.push(method)
if @service_object.respond_to?(method)
@service_object.send(method, *args)
else
raise NotImplementedError
end
end
Here’s where our supporting method gets its information. And finally, we have the supporting method:
def times_called(method)
@call_history.count { |called| called == method }
end
end
There could be many of these. We can use this supporting method to ask our
Hands
instance a question it doesn’t know how to answer.
>> hands = Proxy.new(Hands.new)
>> 3.times { hands.clap }
"👏"
"👏"
"👏"
=> 3
>> hands.times_called(:clap)
=> 3
Our proxy class now has both its own augmented behavior and the behavior of its service object.