making private methods private

So it's a well-known Rubyism that you can actually circumvent private and protected restrictions on instance methods if you simply use the send() method to access them. I wanted to see if it was possible to rewrite send() on a particular class to throw an exception if the method attempting to be accessed was a private method on the class.

Oh yeah, this post is probably not safe for work...

how i did it

I started out with a simple class:

class PrivateParts
  def initialize
    @boobs = "( . )( . )"
    @butts = "(  )(  )"
    @balls = "( )( )"
  end

private
  def peep_show
    @boobs + @butts + @balls
  end
end

In my test, I am asserting that a NoMethodError will be thrown if I attempt to access the method, even with send().

require 'test/unit'

class PrivatePartsTest < Test::Unit::TestCase
  def test_peep_show_cannot_be_called_outside_of_the_class
    parts = PrivateParts.new

    assert_raises(NoMethodError) { parts.peep_show }
    assert_raises(NoMethodError) { parts.send(:peep_show) }
  end
end

This fails, until I overrode PrivateParts.send()...

  def send(method, *args)
    if private_methods.include? method
      raise NoMethodError.new("private method '#{method}' called for #{self}")
    else
      super(method, *args)
    end
  end

What send() is doing here is checking against Object.private_methods, a collection of method names that are all private. If a match is found, an error is thrown, because somebody outside of the class wanted the data returned by PrivateParts.peep_show. Due to Ruby's own clever scoping of the "actual" send method, overriding send() does not affect the classes own internal behavior, as the private method can still be called as a private member within the class. This is illustrated by the following test:

  def test_peep_show_can_be_exposed_by_exposure
    parts = PrivateParts.new

    assert_equal "( . )( . )(  )(  )( )( )", parts.exposure
  end

And accompanying public method PrivateParts.exposure:

  def exposure
    peep_show
  end

These tests both pass, since peep_show is not using PrivateParts.send() to do its bidding. This override, being done in the "public-facing API" of the class, was scoped to just that portion of the codebase. While this overrides Ruby's "code-as-documentation" appraoch and enforces strict private members in a class, it actually takes advantage of such an approach to provide the functionality in a language that does not provide it out-of-the-box.

conclusion

This whole demonstration is available as a Gist. As this is more of a demonstration in meta-programming, if anything, I wouldn't recommend actually using it. This is of course not an example of how I actually write my code. My personal beliefe is that such enforced rules are unnecessary in the real world, and private/protected methods are simply there to tell developers of an intended purpose for the method. It is almost never necessary to call these methods in a test nor in an outside class, so a case such as this should probably never come up, otherwise I think you may have bigger problems on your hands...

Posted on .