Jake Worth

Jake Worth

Augmenting an Object With the Proxy Pattern

Published: February 26, 2021 • Updated: August 21, 2023 3 min read

  • ruby
  • oop

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.

What are your thoughts on proxies? Let me know!


Join 100+ engineers who subscribe for advice, commentary, and technical deep-dives into the world of software.