Книга: Metaprogramming Ruby 2
Назад: Chapter 5: Thursday: Class Definitions
Дальше: Quiz: Class Taboo

Class Definitions Demystified

Where you and Bill tread familiar ground: the Bookworm application and the Ruby object model.

You stumble sleepily into the office, craving your Thursday morning coffee, only to be ambushed by an excited Bill. “Hey!” he says. “Everyone likes the refactorings of Bookworm we did Monday, and the boss wants more. But before we start, let’s go over some theory about class definitions. We’ll begin where we left off Monday: in the Ruby object model.”

Inside Class Definitions

You probably think of a class definition as the place where you define methods. In fact, you can put any code you want in a class definition:

 
class​ MyClass
 
puts ​'Hello'
 
end
<= 
Hello

Class definitions also return the value of the last statement, just like methods and blocks do:

 
result = ​class​ MyClass
 
self
 
end
 
 
result ​# => MyClass

This last example emphasizes a compelling point that you might remember from : in a class (or module) definition, the class itself takes the role of the current object self. Classes and modules are just objects, so why couldn’t a class be self? Keep this point about class definitions and self in mind, because the concept will become useful a bit later.

While we’re on the topic of self, you can learn about a related concept: that of the current class.

The Current Class

As you know, wherever you are in a Ruby program, you always have a current object: self. Likewise, you always have a current class (or module). When you define a method, that method becomes an instance method of the current class.

Although you can get a reference to the current object through self, there’s no equivalent keyword to get a reference to the current class. However, in most situations it’s easy to keep track of the current class just by looking at the code. Here’s how:

This last case is probably the only one that you care about in practice. Indeed, you use it all the time when you open a class with the class keyword, and define methods in the class with def. However, the class keyword has a limitation: it needs the name of a class. Unfortunately, in some situations you may not know the name of the class that you want to open. For example, think of a method that takes a class and adds a new instance method to it:

 
def​ add_method_to(a_class)
 
# TODO: define method m() on a_class
 
end

How can you open the class if you don’t know its name? You need some way other than the class keyword to change the current class. Enter the class_eval method.

class_eval()

Module#class_eval (also known by its alternate name, module_eval) evaluates a block in the context of an existing class:

 
def​ add_method_to(a_class)
 
a_class.class_eval ​do
 
def​ m; ​'Hello!'​; ​end
 
end
 
end
 
 
add_method_to String
 
"abc"​.m ​# => "Hello!"

Module#class_eval is very different from BasicObject#instance_eval, which you learned about earlier in .  instance_eval only changes self, while class_eval changes both self and the current class.

(This is not the whole truth: instance_eval also changes the current class, but you’ll have to wait for , to learn how exactly. For now, you can safely ignore the problem and assume that instance_eval only changes self.)

By changing the current class, class_eval effectively reopens the class, just like the class keyword does.

Module#class_eval is actually more flexible than class. You can use class_eval on any variable that references the class, while class requires a constant. Also, class opens a new scope, losing sight of the current bindings, while class_eval has a Flat Scope (). As you learned in , this means you can reference variables from the outer scope in a class_eval block.

Finally, just like instance_eval has a twin method called instance_exec, module_eval/class_eval also has an equivalent module_exec/class_exec method that can pass extra parameters to the block.

Now that you know about both instance_eval and class_eval, you might wonder which of the two you should use. In most cases the answer is easy: you use instance_eval to open an object that is not a class, and class_eval to open a class definition and define methods with def. But what if you want to open an object that happens to be a class (or module) to do something other than using def? Should you use instance_eval or class_eval then?

If all you want is to change self, then both instance_eval and class_eval will do the job nicely. However, you should pick the method that best communicates your intentions. If you’re thinking, “I want to open this object, and I don’t particularly care that it’s a class,” then instance_eval is fine. If you’re thinking, “I want an Open Class () here,” then class_eval is almost certainly a better match.

That was a lot of information about the current class and how to deal with it. Let’s recap the important points that we just went through.

Current Class Wrap-up

You learned a few things about class definitions:

How can this stuff ever be useful in real life? To show you how you can apply this theory about the current class, let’s look at a trick called Class Instance Variables.

Class Instance Variables

The Ruby interpreter assumes that all instance variables belong to the current object self. This is also true in a class definition:

 
class​ MyClass
 
@my_var = 1
 
end

In a class definition, the role of self belongs to the class itself, so the instance variable @my_var belongs to the class. Don’t get confused. Instance variables of the class are different from instance variables of that class’s objects, as you can see in the following example:

 
class​ MyClass
 
@my_var = 1
 
def​ self.read; @my_var; ​end
 
def​ write; @my_var = 2; ​end
 
def​ read; @my_var; ​end
 
end
 
 
obj = MyClass.new
 
obj.read ​# => nil
 
obj.write
 
obj.read ​# => 2
 
MyClass.read ​# => 1

The previous code defines two instance variables. Both happen to be named @my_var, but they’re defined in different scopes, and they belong to different objects. To see how this works, you have to remember that classes are just objects, and you have to track self through the program. One @my_var is defined with obj as self, so it’s an instance variable of the obj object. The other @my_var is defined with MyClass as self, so it’s an instance variable of the MyClass object—a Spell: .

If you come from Java, you may be tempted to think that Class Instance Variables are similar to Java’s “static fields.” Instead, they’re just regular instance variables that happen to belong to an object of class Class. Because of that, a Class Instance Variable can be accessed only by the class itself—not by an instance or by a subclass.

We’ve touched on many things: the current class, class definitions, self, class_eval, Class Instance Variables.… Now you can go back to Bookworm and put these features together.

Working on Bookworm Again

The Bookworm source contains very few unit tests, so it’s up to you and Bill to write tests as you refactor. Sometimes this proves to be difficult, as is the case with this class:

 
class​ Loan
 
def​ initialize(book)
 
@book = book
 
@time = Time.now
 
end
 
 
def​ to_s
 
"​#{@book.upcase}​ loaned on ​#{@time}​"
 
end
 
end

Loan stores the title of a book and the time when it was loaned—that is, the time when the object was created. You’d like to write a unit test for the to_s method, but to write that test, you’d have to know the exact time when the object was created. This is a common problem with code that relies on Time or Date: such code returns a different result every time it runs, so you don’t know what result to test for.

“I think I have a solution to this problem,” Bill announces. “It’s a bit involved, so it will require some attention on your part. Here it is.”

 
class​ Loan
 
def​ initialize(book)
 
@book = book
*
@time = Loan.time_class.now
 
end
 
*
def​ self.time_class
*
@time_class || Time
*
end
 
 
def​ to_s
 
# ...

Loan.time_class returns a class, and Loan#initialize uses that class to get the current time. The class is stored in a Class Instance Variable () named @time_class. If @time_class is nil, the Nil Guard () in time_class returns the Time class as a default.

In production, Loan always uses the Time class, because @time_class is always nil. By contrast, the unit tests can rely on a fake time class that always returns the same value. The tests can assign a value to the private @time_class variable by using either class_eval or instance_eval. Either of the two methods will do here, because they both change self:

 
class​ FakeTime
 
def​ self.now; ​'Mon Apr 06 12:15:50'​; ​end
 
end
 
 
require ​'test/unit'
 
 
class​ TestLoan < Test::Unit::TestCase
 
def​ test_conversion_to_string
 
Loan.instance_eval { @time_class = FakeTime }
 
loan = Loan.new(​'War and Peace'​)
 
assert_equal ​'WAR AND PEACE loaned on Mon Apr 06 12:15:50'​, loan.to_s
 
end
 
end

Bill is quite proud of his own coding prowess. He says, “I think we deserve a break—after I give you a quiz.”

Назад: Chapter 5: Thursday: Class Definitions
Дальше: Quiz: Class Taboo