ActiveRecord learns to respect your privates

December 2, 2008 Adam Milligan

This is somewhat old news, but I don’t think it has received the attention it deserves. As of Rails 2.2, ActiveRecord associations and attributes will now behave properly with regard to access control. You can view the Rails tickets, with patches, here and here.

Take, for example, this schema:

mysql> desc accounts;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | int(11)      | NO   | PRI | NULL    | auto_increment |
| balance        | int(11)      | NO   |     | 0       |                |
+----------------+--------------+------+-----+---------+----------------+

used by this model:

class Account < ActiveRecord::Base
  def deposit(amount)
    do_state_and_federally_mandated_things
    balance += amount
  end

  def withdraw(amount)
    if sufficient_funds?(amount)
      do_state_and_federally_mandated_things
      balance -= amount
    end
  end

private :balance=

private

  def do_state_and_federally_mandated_things
    ...
  end
end

You most likely don’t want someone coming along and modifying the balance attribute directly, either intentionally or inadvertently. However, prior to Rails 2.2, ActiveRecord ignores the privacy declaration for #balance=, so you must execute horrid machinations in order to protect it:

class Account < ActiveRecord::Base
  def balance=(amount)
    raise "I'm private!"
  end

  def deposit(amount)
    do_state_and_federally_mandated_things
    write_attribute(:balance, balance + amount)
  end

  ...

As of Rails 2.2, ActiveRecord will respect private accessors for database column attributes.

Along the same vein, if we add the following:

class User < ActiveRecord::Base
  has_one :account
end

Now we can call methods on the proxy returned by calling User#account, just as if we were calling methods on an account instance; any methods:

johnny_taxpayer = User.first
johnny_taxpayer.account.withdraw(700_000_000_000)
johnny_taxpayer.account.do_state_and_federally_mandated_things

Who knows what scary things #do_state_and_federally_mandated_things does? This will, frighteningly, run just fine prior to Rails 2.2. But, no more.

Now, a number of people have considered this change and asked “why bother?” Ruby allows access to private methods via #send, so they’re not really private, right?

This argument leads down the dark path. Like it or not, cheat around it as you may, access control is an important aspect of object oriented programming. If nothing else, the private keyword is my way of saying “hic sunt dracones,” or “hands off!” It’s also a way of saying “this method may or may not exist in the future.” As a class designer I have every right to refactor that private method entirely away, thus breaking any code that calls it; including, significantly, test code.

More fundamentally, object interactions should be via interfaces. If code makes calls to an object’s private methods, that code has now tied itself to the object’s implementation. Coupling ensues, duck-typing breaks down, anarchy reigns.

So, this begs the question, why does #send ignore access control?. Why should two forms of sending a message to an object differ in their access control semantics? Sometimes I’m forced to use #send:

method_name = extract_method_name_from_the_aether
some_object.send(method_name)

In order to make this code correct with regard to access control I have to add cruft:

method_name = extract_method_name_from_the_aether
some_object.send(method_name) if some_object.respond_to?(method_name)

Now, this is not to say that I don’t think Ruby should provide the ability to call private methods, or generally dig around in an object’s internals. This comes in handy in some instances (although I find many of these instances are short-term solutions that should get refactored away). But, in an ideal world I think the sender should explicitly specify when a message should ignore access control:

potentially_private = extract_method_name_from_the_aether
some_object.send_without_restriction(potentially_private)

This makes the syntax somewhat more ugly, but ugly syntax suits an ugly operation.

About the Author

Biography

More Content by Adam Milligan
Previous
GitHub post receive hook for Tracker
GitHub post receive hook for Tracker

Chris Bailey wrote a GitHub post receive hook for Tracker, using the API. It's a web service that you run o...

Next
Tracker 101 Screencast
Tracker 101 Screencast

If you're new to Tracker, or are considering trying it out but haven't signed up yet, check out the Tracker...

How do you measure digital transformation?

Take the Benchmark