user1934428 user1934428 - 3 months ago 15
Ruby Question

Use or misuse of private class methods in Ruby

I have a class, where I want one class method be used only by other class and instance methods, but not from other classes - with other words, I would like to have a private class method. After a bit of researching, I came up with the following approach:

class Example
def initialize
end
def f
# My private class method will be called here
self.class.send(:g)
end
# g is going to be a private class method
def self.g
puts 4711
end
private_class_method :g
end


This works so far; calling
Example.new.f
invokes
g
and calling
Example.g
throws an exception, as required.

What is awkward, is the way to call g (
self.class.send(:g)
). Is there a nicer way to do it? Or is it in general a bad idea to make a class method private, when it needs to be called by an instance method?

Answer

There is no such thing as a "class method" in Ruby. There are only instance methods. "Class methods" are actually singleton methods (on an object which just so happens to be an instance of Class), and "singleton methods" are just instance methods of the singleton class.

private methods can only be called with an implicit receiver (which is self), i.e. they can only be called by other methods on the same object (which in turn means they must be methods of the same class or one of its ancestors, i.e. superclasses, prepended modules or included modules).

This means that private "class methods" can only be called by other "class methods", i.e. methods defined in Example's singleton class, Class, Module, Object, Kernel, or BasicObject. You cannot call them from methods defined in Example.

Think about it: what's the purpose of private? Its purpose is encapsulation, we want to encapsulate the internal implementation and representation details from the external protocol. There are two kinds of encapsulation in wide use currently: Abstract Data Type-Oriented Encapsulation (where instances of different types are encapsulated from each other, but instances of the same type can access each other's internals, e.g. classes in Java) and Object-Oriented Encapsulation (where different objects are encapsulated from each other, even if they are instances of the same type, e.g. Ruby and interfaces in Java). Ruby is an object-oriented language, therefore it uses object-oriented encapsulation. (Java OTOH uses ADT-oriented encapsulation for classes, which is counter-intuitive, since it is usually claimed to be an object-oriented language).

In your case, we have two objects, Example and an instance of Example. They are two different objects, so object-oriented encapsulation simply forbids you from accessing one object's private parts from the other object. Even if Ruby did use ADT-oriented encapsulation, it still wouldn't work: ADT-oriented encapsulation allows two instances of the same type to access each other's privates, but the two objects aren't of the same type, one is an instance of Class and the other is an instance of Example.

Basically, you want to manipulate two object's internals at the same time and that is just not possible in OOP. It's a fundamental design principle of OOP that each object (and each object alone) is responsible for its private parts, and you can only interact with that object by sending it messages through its public protocol.

tl;dr: what you want goes directly against the basic encapsulation principles in OO. Either Example::g must be public or you need to change your design. (Resorting to reflective hacks to circumvent access protection in code you have no control over is a code smell at best. Resorting to reflective hacks to circumvent access protection in code you own is just plain wrong, because you control the access protection, you could just change it.)


One possible solution is to leave OOP behind altogether, and look to functional programming for help. We could try and use closures for encapsulation:

We start out with your original example:

class Example
  private_class_method def self.g
    puts 4711
  end

  def f
    self.class.send(:g)
  end
end

Example.new.f
# 4711

Now, we turn g into a local variable and assign a lambda to it, and in turn use that lambda to define f:

class Example
  g = -> {
    puts 4711
  }

  define_method(:f, &g)
end

Example.new.f
# 4711

Now, g is (in some sense) even more "private" than before, because it only exists within the lexical scope of the class body, not even class methods defined in a different class body can access it. However, the lambda being referenced by g is a proper object and can be passed around even into different scopes.

But, presumably you don't want f and g to be just identical (otherwise you could just have used module_function, after all), instead you want f to do something other than just delegate to g. That is also possible:

class Example
  g = -> {
    puts 4711
  }

  define_method(:f) do
    puts 42
    g.()
  end
end

Example.new.f
# 42
# 4711

This works, because in some other sense g is less "private" than before: lexical scopes can nest, in particular the lexical scopes of blocks (and only blocks) nest, so that the nested block (the block passed to define_method in this case) can access the lexical environment of the outer lexical scope (the class body in this case) even after that lexical scope no longer exists (the class body has finished evaluating).

Comments