Where you’re unexpectedly left alone to develop a new version of the RedFlag DSL.
Your boss wants you to add a setup instruction to the RedFlag DSL, as shown in the following code.
| setup do |
| puts "Setting up sky" |
| @sky_height = 100 |
| end |
| |
| setup do |
| puts "Setting up mountains" |
| @mountains_height = 200 |
| end |
| |
| event "the sky is falling" do |
| @sky_height < 300 |
| end |
| |
| event "it's getting closer" do |
| @sky_height < @mountains_height |
| end |
| |
| event "whoops... too late" do |
| @sky_height < 0 |
| end |
In this new version of the DSL, you’re free to mix events and setup blocks (setups for short). The DSL still checks events, and it also executes all the setups before each event. If you run redflag.rb on the previous test file, you expect this output:
<= | Setting up sky |
| Setting up mountains |
| ALERT: the sky is falling |
| Setting up sky |
| Setting up mountains |
| ALERT: it's getting closer |
| Setting up sky |
| Setting up mountains |
RedFlag executes all the setups before each of the three events. The first two events generate an alert; the third doesn’t.
A setup can set variables by using variable names that begin with an @ sign, such as @sky_height and @mountains_height. Events can then read these variables. Your boss thinks that this feature will encourage programmers to write clean code: all shared variables are initialized together in a setup and then used in events, so it’s easy to keep track of variables.
Still impressed by your boss’ technical prowess, you and Bill get down to business.
You and Bill compare the current RedFlag DSL to the new version your boss has suggested. The current RedFlag executes blocks immediately. The new RedFlag should execute the setups and the events in a specific order. You start by rewriting the event method:
| def event(description, &block) |
| @events << {:description => description, :condition => block} |
| end |
| |
| @events = [] |
| load 'events.rb' |
The new event method converts the event condition from a block to a Proc. Then it wraps the event’s description and the Proc-ified condition in a hash and stores the hash in an array of events. The array is a top-level instance variable (like the ones you read about in ), so it can be initialized outside the event method. Finally, the last line loads the file that defines the events. Your plan is to write a setup method similar to the event method, and then write the code that executes events and setups in the correct sequence.
As you ponder your next step, Bill slaps his forehead, mutters something about his wife’s birthday party, and runs out the door. Now it’s up to you alone. Can you complete the new RedFlag DSL and get the expected output from the test file?
You can find many different solutions to this quiz. Here is one:
| def setup(&block) |
| @setups << block |
| end |
| |
| def event(description, &block) |
| @events << {:description => description, :condition => block} |
| end |
| |
| @setups = [] |
| @events = [] |
| load 'events.rb' |
| |
| @events.each do |event| |
| @setups.each do |setup| |
| setup.call |
| end |
| puts "ALERT: #{event[:description]}" if event[:condition].call |
| end |
Both setup and event convert the block to a proc and store away the proc, in @setups and @events, respectively. These two top-level instance variables are shared by setup, event, and the main code.
The main code initializes @setups and @events, then it loads events.rb. The code in the events file calls back into setup and event, adding elements to @setups and @events.
With all the events and setups loaded, your program iterates through the events. For each event, it calls all the setup blocks, and then it calls the event.
You can almost hear the voice of Bill in your head, sounding a bit like Obi-Wan Kenobi: “Those top-level instance variables, @events and @setups, are like global variables in disguise. Why don’t you get rid of them?”
To get rid of the global variables (and Bill’s voice in your head), you can use a Shared Scope ():
| lambda { |
| setups = [] |
| events = [] |
| |
| Kernel.send :define_method, :setup do |&block| |
| setups << block |
| end |
| |
| Kernel.send :define_method, :event do |description, &block| |
| events << {:description => description, :condition => block} |
| end |
| |
| Kernel.send :define_method, :each_setup do |&block| |
| setups.each do |setup| |
| block.call setup |
| end |
| end |
| |
| Kernel.send :define_method, :each_event do |&block| |
| events.each do |event| |
| block.call event |
| end |
| end |
| }.call |
| |
| load 'events.rb' |
| |
| each_event do |event| |
| each_setup do |setup| |
| setup.call |
| end |
| puts "ALERT: #{event[:description]}" if event[:condition].call |
| end |
The Shared Scope is contained in a lambda that is called immediately. The code in the lambda defines the RedFlag methods as Kernel Methods () that share two variables: setups and events. Nobody else can see these two variables, because they’re local to the lambda. (Indeed, the only reason why we have a lambda here is that we want to make these variables invisible to anyone except the four Kernel Methods.) And yes, each call to Kernel.send is passing a block as an argument to another block.
Now those ugly global variables are gone, but the RedFlag code is not as pleasantly simple as it used to be. It’s up to you to decide whether this change is an improvement or just an unwelcome obfuscation. While you decide that, there is one last change that is worth considering.
In the current version of RedFlag, events can change each other’s shared top-level instance variables:
| event "define a shared variable" do |
| @x = 1 |
| end |
| event "change the variable" do |
| @x = @x + 1 |
| end |
You want events to share variables with setups, but you don’t necessarily want events to share variables with each other. Once again, it’s up to you to decide whether this is a feature or a potential bug. If you decide that events should be as independent from each other as possible (like tests in a test suite), then you might want to execute events in a Clean Room ():
| each_event do |event| |
| env = Object.new |
| each_setup do |setup| |
| env.instance_eval &setup |
| end |
| puts "ALERT: #{event[:description]}" if env.instance_eval &(event[:condition]) |
| end |
Now an event and its setups are evaluated in the context of an Object that acts as a Clean Room. The instance variables in the setups and events are instance variables of the Clean Room rather than top-level instance variables. Because each event runs in its own Clean Room, events cannot share instance variables.
You might think of using a BasicObject instead of an Object for your Clean Room. However, remember that BasicObject is also a Blank Slate (), and as such it lacks some common methods, such as puts. So you should only use a BasicObject if you know that the code in the RedFlag events isn’t going to call puts or other Object methods. You grin and add a comment to the code, leaving this difficult decision to Bill.