Let's get back to our jukebox for a moment (remember the
jukebox?).
At some point we'll be working on the code that handles the
user interface---the buttons that people press to select songs and
control the jukebox. We'll need to associate actions with those
buttons: press
STOP and the music stops. It turns out that
Ruby's blocks are a convenient way to do this. Let's start out by
assuming that the people who made the hardware implemented a Ruby
extension that gives us a basic button
class. (We talk about extending Ruby beginning on page 169.)
bStart = Button.new("Start")
bPause = Button.new("Pause")
# ...
|
What happens when the user presses one of our buttons? In the
Button
class, the hardware folks rigged things so that a
callback method,
buttonPressed
, will be invoked.
The obvious way of adding functionality to these buttons is to create
subclasses of
Button
and have each subclass implement its own
buttonPressed
method.
class StartButton < Button
def initialize
super("Start") # invoke Button's initialize
end
def buttonPressed
# do start actions...
end
end
bStart = StartButton.new
|
There are two problems here. First, this will lead to a large number
of subclasses. If the interface to
Button
changes, this could
involve us in a lot of maintenance. Second, the actions performed when
a button is pressed are expressed at the wrong level; they are not a
feature of the button, but are a feature of the jukebox that uses the
buttons. We can fix both of these problems using blocks.
class JukeboxButton < Button
def initialize(label, &action)
super(label)
@action = action
end
def buttonPressed
@action.call(self)
end
end
bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }
|
The key to all this is the second parameter to
JukeboxButton#initialize
. If the last parameter in a method
definition is prefixed with an ampersand (such as
&action
),
Ruby
looks for a code block whenever that method is called. That code block
is converted to an object of class
Proc
and assigned to the
parameter. You can then treat the parameter as any other variable. In
our example, we assigned it to the instance variable
@action
.
When the callback method
buttonPressed
is invoked, we use the
Proc#call
method on that object to invoke the block.
So what exactly do we have when we create a
Proc
object? The
interesting thing is that it's more than just a chunk of code.
Associated with a block (and hence a
Proc
object) is all the
context in which the block was
defined: the value of
self
, and the methods, variables, and constants in scope. Part
of the magic of Ruby is that the block can still use all this original
scope information even if the environment in which it was defined
would otherwise have disappeared. In other languages, this facility
is called a
closure.
Let's look at a contrived example. This example uses the method
proc
,
which converts a block to a
Proc
object.
def nTimes(aThing)
|
return proc { |n| aThing * n }
|
end
|
|
p1 = nTimes(23)
|
p1.call(3)
|
� |
69
|
p1.call(4)
|
� |
92
|
p2 = nTimes("Hello ")
|
p2.call(3)
|
� |
"Hello Hello Hello "
|
The method
nTimes
returns a
Proc
object that references
the method's parameter,
aThing
. Even though that parameter is out
of scope by the time the block is called, the parameter remains
accessible to the block.