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
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 AuthorMore Content by Adam Milligan