Книга: Metaprogramming Ruby 2
Назад: Ghost Towns and Marketplaces
Дальше: Metaprogramming and Ruby

The Story of Bob, Metaprogrammer

Bob, a newcomer to Ruby, has a grand plan: he’ll write the biggest Internet social network ever for movie buffs. To do that, he needs a database of movies and movie reviews. Bob makes it a practice to write reusable code, so he decides to build a simple library to persist objects in the database.

Bob’s First Attempt

Bob’s library maps each class to a database table and each object to a record. When Bob creates an object or accesses its attributes, the object generates a string of SQL and sends it to the database. All this functionality is wrapped in a class:

 
class​ Entity
 
attr_reader :table, :ident
 
 
def​ initialize(table, ident)
 
@table = table
 
@ident = ident
 
Database.sql ​"INSERT INTO ​#{@table}​ (id) VALUES (​#{@ident}​)"
 
end
 
 
def​ set(col, val)
 
Database.sql ​"UPDATE ​#{@table}​ SET ​#{col}​='​#{val}​' WHERE id=​#{@ident}​"
 
end
 
 
def​ get(col)
 
Database.sql (​"SELECT ​#{col}​ FROM ​#{@table}​ WHERE id=​#{@ident}​"​)[0][0]
 
end
 
end

In Bob’s database, each table has an id column. Each Entity stores the content of this column and the name of the table to which it refers. When Bob creates an Entity, the Entity saves itself to the database. Entity#set generates SQL that updates the value of a column, and Entity#get generates SQL that returns the value of a column. (In case you care, Bob’s Database class returns recordsets as arrays of arrays.)

Bob can now subclass Entity to map to a specific table. For example, class Movie maps to a database table named movies:

 
class​ Movie < Entity
 
def​ initialize(ident)
 
super​ ​"movies"​, ident
 
end
 
 
def​ title
 
get ​"title"
 
end
 
 
def​ title=(value)
 
set ​"title"​, value
 
end
 
 
def​ director
 
get ​"director"
 
end
 
 
def​ director=(value)
 
set ​"director"​, value
 
end
 
end

A Movie has two methods for each attribute: a reader, such as Movie#title, and a writer, such as Movie#title=. Bob can now load a new movie into the database by firing up the Ruby interactive interpreter and typing the following:

 
movie = Movie.new(1)
 
movie.title = ​"Doctor Strangelove"
 
movie.director = ​"Stanley Kubrick"

This code creates a new record in movies, which has values 1, Doctor Strangelove, and Stanley Kubrick for the columns id, title, and director, respectively. (Remember that in Ruby, movie.title = "Doctor Strangelove" is actually a disguised call to the method title=—the same as movie.title=("Doctor Strangelove").)

Proud of himself, Bob shows the code to his older, more experienced colleague, Bill. Bill stares at the screen for a few seconds and proceeds to shatter Bob’s pride into tiny little pieces. “There’s a lot of duplication in this code,” Bill says. “You have a movies table with a title column in the database, and you have a Movie class with an @title field in the code. You also have a title method, a title= method, and two "title" string constants. You can solve this problem with way less code if you sprinkle some metaprogramming over it.”

Enter Metaprogramming

At the suggestion of his expert-coder friend, Bob looks for a metaprogramming-based solution. He finds that very thing in the Active Record library, a popular Ruby library that maps objects to database tables. After a short tutorial, Bob is able to write the Active Record version of the Movie class:

 
class​ Movie < ActiveRecord::Base
 
end

Yes, it’s as simple as that. Bob just subclassed the ActiveRecord::Base class. He didn’t have to specify a table to map Movies to. Even better, he didn’t have to write boring, almost identical methods such as title and director. It all just works:

 
movie = Movie.create
 
movie.title = ​"Doctor Strangelove"
 
movie.title ​# => "Doctor Strangelove"

The previous code creates a Movie object that wraps a record in the movies table, then accesses the record’s title column by calling Movie#title and Movie#title=. But these methods are nowhere to be found in the source code. How can title and title= exist if they’re not defined anywhere? You can find out by looking at how Active Record works.

The table name part is straightforward: Active Record looks at the name of the class through introspection and then applies some simple conventions. Since the class is named Movie, Active Record maps it to a table named movies. (This library knows how to find plurals for English words.)

What about methods such as title= and title, which access object attributes (accessor methods for short)? This is where metaprogramming comes in: Bob doesn’t have to write those methods. Active Record defines them automatically, after inferring their names from the database schema. ActiveRecord::Base reads the schema at runtime, discovers that the movies table has two columns named title and director, and defines accessor methods for two attributes of the same name. This means that Active Record defines methods such as Movie#title and Movie#director= out of thin air while the program runs.

This is the “yang” to the introspection “yin”: rather than just reading from the language constructs, you’re writing into them. If you think this is an extremely powerful feature, you are right.

The “M” Word Again

Now you have a more formal definition of metaprogramming:

Metaprogramming is writing code that manipulates language constructs at runtime.

The authors of Active Record applied this concept. Instead of writing accessor methods for each class’s attributes, they wrote code that defines those methods at runtime for any class that inherits from ActiveRecord::Base. This is what I meant when I talked about “writing code that writes code.”

You might think that this is exotic, seldom-used stuff—but if you look at Ruby, as we’re about to do, you’ll see that it’s used frequently.

Назад: Ghost Towns and Marketplaces
Дальше: Metaprogramming and Ruby