At about the 19:00 mark in his RailsConf presentation, David Heinemeier Ha开发者_运维问答nsson talks about the downsides of instance_eval
:
For a long time I ranted and raved against
instance_eval
, which is the concept of not using a yielded parameter (likedo |people|
) and just straightdo something
and then evaluate what's in that block within the scope of where you came from (I don't even know if that's a coherent explanation)For a long time I didn't like that because it felt more complex in some sense. If you wanted to put your own code in there were you going to trigger something that was already there? Were you going to override something? When you're yielding a specific variable you can chain everything off that and you can know [you're] not messing with anybody else's stuff
This sounded interesting, but a) I don't know how how instance_eval
works in the first place and b) I don't understand why it can be bad / increase complexity.
Can someone explain?
The thing that instance_eval
does is that it runs the block in the context of a different instance. In other words, it changes the meaning of self
which means it changes the meaning of instance methods and instance variables.
This creates a cognitive disconnect: the context in which the block runs is not the context in which it appears on the screen.
Let me demonstrate that with a slight variation of @Matt Briggs's example. Let's say we're building an email instead of a form:
def mail
builder = MailBuilder.new
yield builder
# executed after the block
# do stuff with builder
end
mail do |f|
f.subject @subject
f.name name
end
In this case, @subject
is an instance variable of your object and name
is a method of your class. You can use nice object-oriented decomposition and store your subject in a variable.
def mail &block
builder = MailBuilder.new
builder.instance_eval &block
# do stuff with builder
end
mail do
subject @subject
name name # Huh?!?
end
In this case, @subject
is an instance variable of the mail builder object! It might not even exist! (Or even worse, it might exist and contain some completely stupid value.) There is no way for you to get access to your object's instance variables. And how do you even call the name
method of your object? Everytime you try to call it, you get the mail builder's method.
Basically, instance_eval
makes it hard to use your own code inside the DSL code. So, it should really only be used in cases where there is very little chance that this might be needed.
Ok, so the idea here is instead of something like this
form_for @obj do |f|
f.text_field :field
end
you get something like this
form_for @obj do
text_field :field
end
the first way is pretty straight forward, you end up with a pattern that looks like this
def form_for
b = FormBuilder.new
yield b
b.fields.each |f|
# do stuff
end
end
you yield out a builder object that the consumer calls methods on, and afterwards you call methods on the builder object to actually build the form (or whatever)
the second one is a bit more magical
def form_for &block
b = FormBuilder.new
b.instance_eval &block
b.fields.each |f|
#do stuff
end
end
in this one, instead of yielding the builder to the block, we take the block and evaluate it in the context of the builder
The second one increases complexity because you are sort of playing games with scope, you need to understand that, and the consumer needs to understand that, and whoever wrote your builder needs to understand that. If everyone is on the same page, I don't know that it is nessicarily a bad thing, but i do question the benefits vs the costs, i mean, how hard is it to just tack on an f. in front of your methods?
The idea is that it's a little dangerous in that you can never be quite sure you're not going to break something without reading all the code that deals with the object your using instance_eval on.
Also if you , say, updated a library that didn't change the interface much but changed a lot of the object internals you could really do some damage.
精彩评论