views:

321

answers:

4

I am still new to Ruby and basically just writing my first micro-program after finishing Cooper's book. I was pointed to the direction of avoiding monkey patching but the problem is I don't know what are the alternatives to achieve the same behavior. Basically, I want to add a new method that is accessible by every string object. The obvious monkey-patching way is to:

class String
  def do_magic
    ...magic...
  end
end

I recall there's a way using String.send. But I can't remember how it's done nor where I read it. Can anyone point out any alternatives that would still let me make that method available to the String class and child objects?

+6  A: 

Any other way of doing this would just be a more awkward syntax for monkey patching. There are ways involving send and eval and all sorts of things, but why? Go ahead and do it the obvious way.

You want to be careful of monkey patching in big projects or when you have dependencies, because you can wind up with conflicts when several hands are all messing around in the same place. This doesn't mean look for an alternative syntax that accomplishes the same thing — it means be careful when you're making changes that could affect code that's not yours. This probably isn't a concern in your particular case. It's just something that might need to be addressed in larger projects.

One alternative in Ruby is that you can add methods to a single object.

a = "Hello"
b = "Goodbye"
class <<a
  def to_slang
    "yo"
  end
end
a.to_slang # => "yo"
b.to_slang # NoMethodError: undefined method `to_slang' for "Goodbye":String
Chuck
I can see how it really depends on the situation and the nature of the project itself. And I think this would also apply to all of the do/don't pairs.Considering my particular case, yes, it makes sense that it should not hurt. After all, I am adding a new method. Thanks for the clarification!
dmondark
+1  A: 

The object class defines send, and all objects inherit this. You "send" an object the send method. The send method's parameters are the method-you-want-to-invoke's name as a symbol, followed by any arguments and an optional block. You can also use __send__.

>> "heya".send :reverse
=> "ayeh"

>> space = %w( moon star sun galaxy )
>> space.send(:collect) { |el| el.send :upcase! }
=> ["MOON", "STAR", "SUN", "GALAXY"]

Edit..

You probably want to use the define_method method:

String.class_eval {
  define_method :hello do |name|
    self.gsub(/(\w+)/,'hello') + " #{name}"
  end
}

puts "Once upon a time".hello("Dylan")
# >> hello hello hello hello Dylan

That adds instance methods. To add class methods:

eigenclass = class << String; self; end
eigenclass.class_eval {
  define_method :hello do
    "hello"
  end
}

puts String.hello
# >> hello

You can't define methods that expect a block though.

It might be a good thing to have a read of this chapter from Why's Poignant Guide, you can skip down to "Dwemthy’s Array" to get to the meta-programming stuff.

dylanfm
I was trying to temporarily avoid jumping into meta-programming just yet. But I guess it's just inevitable if I want to get past hello-world-ish stuff.My idea about using a evals is bascially runtime-monkeypatching. But I now see how everything else is just that.
dmondark
+3  A: 

If you want to add a new method that is accessible to every string object, then doing it the way you have it is how to get it done.

A good practice is to put your extensions to core objects in a separate file (like string_ex.rb) or a sub-directory(like extensions or core_ext). This way, it is obvious what has been extended, and it is easy for someone to see how they have been extended or changed.

Where monkey patching can go bad is when you change some existing behavior of a core object that causes some other code that expects the original functionality to misbehave.

Aaron Hinni
+1  A: 

Thanks guys.

All of the suggested implementation work. More importantly, I learned to weigh in the case in hand and decide if re-opening core (or library) classes is a good idea or not.

FWIW, a friend pointed out the send implementation I was looking for. But now that I look at it, it's even closer to monkeypatching than all the other implementations :)

module M
    def do_magic
    ....
    end
end
String.send(:include, M)
dmondark