Don't use Metaprogramming When You Can Just Throw Classes at Things
October 28, 2014
Ruby has very powerful support for metaprogramming, which is also known as code that writes code, but it can be difficult to understand what is going on. One of the more obscure features of Object-Oriented Programming is the Singleton Pattern, where you can add behavior to specific instances of classes and not all instances.
x = "wombat"
y = "wobbegong"
def x.foo
"foo"
end
x.foo # => "foo"
y.foo # => NoMethodError: undefined method `foo' for "wobbegong":String
What’s going on here is you are defining #foo
on the instance of String “wombat” but nothing else. When you define this method, internally Ruby is inserting a Singleton class with that method into the objects inheritance structure. When you call this method, Ruby doesn’t find it in the String class and it walks up the tree of inheritance till it either finds the method you called, or it calls #method_missing
which defaults to raising that NoMethodError
we saw earlier. This is not the only way to define Singleton methods.
x = "wombat"
x.define_singleton_method(:foo) do
"foo"
end
x.foo # => "wombat"
You can even do this to a bunch of instances all at once.
module Animals
X = "wombat"
Y = "wobbegong"
Z = "giant salamander"
constants.each do |constant|
eval(constant.to_s).define_singleton_method(:foo) do
"foo"
end
end
end
#each
is looking through every constant defined in the module and adding a singleton class #foo
to them. The way the .constants
method works is it grabs all of the constants as symbols, which won’t work on #define_singleton_class
, so they must be evaluating with eval
, which executes any piece of code. If you use eval to parse something a client can enter, you open yourself up to code injection, which is marginally worse than SQL injection.
You could probably think of a few different ways to do this, such as opening up classes, which is also derogatorily called Monkey Patching.
class String
def foo
"foo"
end
end
Now every time you call #foo
on any String instance, you get "foo"
.
While these features seem really cool, there really isn’t a great use for the Singleton Pattern here because we can just throw more classes at it.
class Foo
attr_reader :string
def initialize(string)
@string = string
end
def foo
"foo"
end
def to_s
string
end
end
x = Foo.new("wombat")
x # => wombat
x.foo # => "foo"
The best parts about this is you can clearly see what is going on, and you can see that you didn’t break anything in any other classes.
This is not to say don’t use metaprogramming, just don’t use it to take away clarity from what is going on.