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 SymbolThe 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.
def %(arg)
...
end
end
module SymbolExtensionGranted, this isn't a huge hint, but if you check
def %(arg)
...
end
end
Symbol.send :include, SymbolExtension
Symbol.ancestors
you'll find the following list.SymbolMany people believe this is better than nothing, but the choice is yours.
SymbolExtension
Object
Kernel
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 SymbolThe 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.
def self.method_added(method)
raise caller.to_s if method == :%
end
end
class Symbol
def %(arg)
self
end
end
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
Thanks, Jay! That helps. Yep, I guess conventions go a long way in keeping such things managed.
ReplyDeleteI'm using a similar convention to this, and also using namespaced modules w/ the corresponding directory structure so rails dependency loading works right.
ReplyDeleteOne issue, though -- how do you handle extending builtin modules, such as Kernel? Kernel.send :include doesn't have the right effect...
Rob,
ReplyDeleteWhen adding behavior to modules I find myself simply opening them instead of using include...