Saturday, January 27, 2007

Class Reopening Hints

In my previous entry about Class Definitions Aman King left the following comment:
I've always liked Ruby's open class feature but have also wondered how do you keep track of all the changes made to the class definition during runtime? ...
There isn't an easy way (that I know of) out of the box to know what files made what changes to your objects. But, are a few things that you can do that may give you hints as to where behavior is coming from.

Use modules instead of adding behavior directly.
On my current project we needed to add a % method to symbol. You can accomplish this with the following code.
class Symbol
def %(arg)
...
end
end
The above code does what you need, but leaves no hint that you've made a change to Symbol. An alternative solution is to define a module and include that module in Symbol.
module SymbolExtension
def %(arg)
...
end
end

Symbol.send :include, SymbolExtension
Granted, this isn't a huge hint, but if you check Symbol.ancestors you'll find the following list.
Symbol
SymbolExtension
Object
Kernel
Many people believe this is better than nothing, but the choice is yours.

Use the stack trace.
Another option is to use the stack trace to try to track down a specific method. If you are working with some code where a method is being modified somewhere, but it isn't in a module the following code could help you out.
class Symbol
def self.method_added(method)
raise caller.to_s if method == :%
end
end

class Symbol
def %(arg)
self
end
end
The above code uses the method_added hook method and raises and exception when the method you are looking for is defined. At this point the stack trace should show you where the method was defined.

Use conventions.
In practice I've had very little trouble with finding where behavior is defined. In all the projects I've been involved with we have a few conventions that we follow that help ensure we can find behavior definitions easily.
  • Define behavior for the class PhoneNumber (for example) in the phone_number.rb file
  • If you need to open a class that you have not created, create a [Class]Extension module and include it as shown above.
  • Avoid dynamically adding behavior in unexpected places when possible. For example we don't define behavior in associations, such as:
    # not recommended
    has_many :people do
    def by_name
    find(:all)
    end
    end
As I previously stated, I'm on a 14 person team currently. Those 14 people create quite a few features at a rapid pace. Despite our size and rapid pace, I can't remember ever having trouble finding behavior.

3 comments:

  1. Anonymous8:02 AM

    Thanks, Jay! That helps. Yep, I guess conventions go a long way in keeping such things managed.

    ReplyDelete
  2. Anonymous5:06 PM

    I'm using a similar convention to this, and also using namespaced modules w/ the corresponding directory structure so rails dependency loading works right.

    One issue, though -- how do you handle extending builtin modules, such as Kernel? Kernel.send :include doesn't have the right effect...

    ReplyDelete
  3. Anonymous3:14 PM

    Rob,

    When adding behavior to modules I find myself simply opening them instead of using include...

    ReplyDelete

Note: Only a member of this blog may post a comment.