Nithin Bekal About

Make delegated methods private in Rails

07 Feb 2019

Recently, I came across some of my old code that uses Rails’ delegate method. Take a look at the following example, where I’m delegating a couple of methods to an instance variable, but want to make them private.

class UserDecorator
  def initialize(user)
    @user = user
  end

  def full_name
    "#{first_name} #{last_name}"
  end

  private

  delegate :first_name, :last_name, to: :@user
end

My intention was to make the first_name and last_name methods private. Only full_name was supposed to be public here. Let’s see if this works as intended.

user = User.new(first_name: 'John', last_name: 'Doe')
decorated_user = UserDecorator.new(user)

decorated_user.full_name #=> John Doe
decorated_user.first_name #=> John
decorated_user.last_name #=> Doe

decorated_user.methods - Object.instance_methods
#=> [:full_name, :last_name, :first_name]

As you can see, we can call both the delegated methods. They were supposed to be private! What happened here?

The problem is that putting delegate under the private scope has no effect. Methods in Ruby have no way of knowing that the private visibility scope is set when they are called from within a class. delegate uses module_eval to define new methods with these names, but it doesn’t know that it must make these methods private.

So what do we do if we wish to delegate a method without making it a part of the class’ public interface? Let’s dig into how delegate works. In cases like this, I like to use the pry console to interactively inspect the behavior of the class.

pry> cd UserDecorator
# This pry command puts us within the scope of the UserDecorator class.

pry> delegate :first_name, :last_name, to: :@user
#=> [:first_name, :last_name]

pry> instance_methods
#=> [:full_name, :last_name, :first_name]

pry> private :first_name, :last_name

pry> instance_methods
#=> [:full_name]

delegate returns the list of the delegated method names. We can call private with the method names to make them private. This means we can explicitly make the methods private like this:

class UserDecorator
  # def initialize...

  delegate :first_name, :last_name, to: :@user
  private :first_name, :last_name
end

Now, how can we avoid duplicating the list of method names? Since delegate returns the list of method names, we can pass the list to private using the splat operator.

class UserDecorator
  # def initialize...

  private *delegate(:first_name, :last_name, to: :@user)
end

Although this prevents duplication, I don’t find this syntax particularly intuitive. Luckily, Rails 6 is introducing a new private: true keyword argument to give us a cleaner syntax.

# Rails 6+
delegate :first_name, :last_name, to: :@user, private: true

However, as I write this post, Rails 6 isn’t out yet. If you’re on Rails 5.2 or below, you’ll have to use the private *delegate(...) syntax for now.

Hi, I’m Nithin! This is my blog about programming. Ruby is my programming language of choice and the topic of most of my articles here, but I occasionally also write about Elixir, and sometimes about the books I read. You can use the atom feed if you wish to subscribe to this blog or follow me on Mastodon.