Книга: Metaprogramming Ruby 2
Назад: Blocks Are Closures
Дальше: Callable Objects

instance_eval()

Where you learn another way to mix code and bindings at will.

The following program demonstrates BasicObject#instance_eval, which evaluates a block in the context of an object:

 
class​ MyClass
 
def​ initialize
 
@v = 1
 
end
 
end
 
 
obj = MyClass.new
 
 
obj.instance_eval ​do
 
self ​# => #<MyClass:0x3340dc @v=1>
 
@v ​# => 1
 
end

The block is evaluated with the receiver as self, so it can access the receiver’s private methods and instance variables, such as @v. Even if instance_eval changes self, the block that you pass to instance_eval can still see the bindings from the place where it’s defined, like any other block:

 
v = 2
 
obj.instance_eval { @v = v }
 
obj.instance_eval { @v } ​# => 2

The three lines in the previous example are evaluated in the same Flat Scope (), so they can all access the local variable v—but the blocks are evaluated with the object as self, so they can also access obj’s instance variable @v. In all these cases, you can call the block that you pass to instance_eval a Spell: , because it’s like a snippet of code that you dip inside an object to do something in there.

Breaking Encapsulation

At this point, you might be horrified. With a Context Probe (), you can wreak havoc on encapsulation! No data is private data anymore. Isn’t that a Very Bad Thing?

Pragmatically, there are some situations where encapsulation just gets in your way. For one, you might want to take a quick peek inside an object from an irb command line. In a case like this, breaking into the object with instance_eval is often the shortest route.

Another acceptable reason to break encapsulation is arguably testing. Here’s an example.

The Padrino Example

The Padrino web framework defines a Logger class that manages all the logging that a web application must deal with. The Logger stores its own configuration into instance variables. For example, @log_static is true if the application must log access to static files.

Padrino’s unit tests need to change the configuration of the application’s logger. Instead of going through the trouble of creating and configuring a new logger, the following tests (written with the RSpec test gem) just pry open the existing application logger and change its configuration with a Context Probe:

 
describe ​"PadrinoLogger"​ ​do
 
context ​'for logger functionality'​ ​do
 
context ​"static asset logging"​ ​do
 
should ​'not log static assets by default'​ ​do
 
# ...
 
get ​"/images/something.png"
 
assert_equal ​"Foo"​, body
 
assert_match ​""​, Padrino.logger.log.string
 
end
 
 
should ​'allow turning on static assets logging'​ ​do
 
Padrino.logger.instance_eval{ @log_static = true }
 
# ...
 
get ​"/images/something.png"
 
assert_equal ​"Foo"​, body
 
assert_match /GET/, Padrino.logger.log.string
 
Padrino.logger.instance_eval{ @log_static = false }
 
end
 
end
 
 
# ...

The first test accesses a static file and checks that the logger doesn’t log anything. This is Padrino’s default behavior. The second test uses instance_eval to change the logger’s configuration and enable static file logging. Then it accesses the same URL as the first test and checks that the logger actually logged something. Before exiting, the second test resets static file logging to the default false state.

You can easily criticize these tests for being fragile: if the implementation of Logger changes and the @log_static instance variable disappears, then the test will break. Like many other things in Ruby, encapsulation is a flexible tool that you can choose to ignore, and it’s up to you to decide if and when to accept that risk. The authors of Padrino decided that a quick hack inside the logger object was an acceptable workaround in this case.

Clean Rooms

Sometimes you create an object just to evaluate blocks inside it. An object like that can be called a Spell: :

 
class​ CleanRoom
 
def​ current_temperature
 
# ...
 
end
 
end
 
 
clean_room = CleanRoom.new
 
clean_room.instance_eval ​do
 
if​ current_temperature < 20
 
# TODO: wear jacket
 
end
 
end

A Clean Room is just an environment where you can evaluate your blocks. It can expose a few useful methods that the block can call, such as current_temperature in the example above. However, the ideal Clean Room doesn’t have many methods or instance variables, because the names of those methods and instance variables could clash with the names in the environment that the block comes from. For this reason, instances of BasicObject usually make for good Clean Rooms, because they’re Blank Slates ()—so they barely have any method at all.

(Interestingly, BasicObject is even cleaner than that: in a BasicObject, standard Ruby constants such as String are out of scope. If you want to reference a constant from a BasicObject, you have to use its absolute path, such as ::String.)

You’ll find a practical example of a Clean Room in .

That’s all you have to know about instance_eval. Now you can move on to the last topic in today’s roadmap: callable objects.

Назад: Blocks Are Closures
Дальше: Callable Objects