Block Pattern #3: Beautiful Object Initialization

There are a couple of ways to initialize an object in Ruby. Oftentimes, this is under the guise of applying configuration on an object. They usually mean the same thing. Here’s an example taken from the Twitter Ruby Gem:[4]

 client = Twitter::REST::Client.new ​do​ |config|
  config.consumer_key = ​"YOUR_CONSUMER_KEY"
  config.consumer_secret = ​"YOUR_CONSUMER_SECRET"
  config.access_token = ​"YOUR_ACCESS_TOKEN"
  config.access_token_secret = ​"YOUR_ACCESS_SECRET"
 end

See if you can guess at how this is implemented. Here’s a hint: consumer_key and the rest belong to accessors of Twitter::REST::Client. You’re going to build the bare minimum to get the preceding code to work. The code is doing two things:

images/blocks/block_patterns_-_object_instantiation/block_patterns_-_object_instantiation_001.jpg

The object instantiation bit is trivial. What’s interesting here is that the initializer of Twitter::REST::Client accepts a block. This block takes a single argument called config. Within the block body, the fields of the config object are set using various values.

Implementing an Object Initialization DSL

Create a file called object_init.rb. You will need to set up the modules and class for Twitter::REST::Client:

 module​ Twitter
 module​ REST
 class​ Client
 end
 end
 end
 
 client = Twitter::REST::Client.new ​do​ |config|
  config.consumer_key = ​"YOUR_CONSUMER_KEY"
  config.consumer_secret = ​"YOUR_CONSUMER_SECRET"
  config.access_token = ​"YOUR_ACCESS_TOKEN"
  config.access_token_secret = ​"YOUR_ACCESS_SECRET"
 end

If you try to run this code, you won’t get any errors. Why? Because Ruby ignores the block when it’s not called within the method body, since there is no yield (yet). Let’s make that block do something. We know from looking at it that:

  1. It is being called from the initializer.

  2. It accepts one single argument, the config object.

  3. The config object has a couple of setters, such as consumer_key.

    The main thing to realize is that the config object can be the same instance created by Twitter::REST::Client.new. Why the can? The short answer is that you can make things a bit more complicated by passing in a configuration object, but let’s stick to simple. Therefore, you now know one additional thing:

  4. config and the instantiated object can be the same thing. Now you should have a better idea:

     module​ Twitter
     module​ REST
     class​ Client
    »attr_accessor​ ​:consumer_key​, ​:consumer_secret​,
    »:access_token​, ​:access_token_secret
    »
    »def​ initialize
    »yield​ self
    »end
     end
     end
     end
     
     client = Twitter::REST::Client.new ​do​ |config|
      config.consumer_key = ​"YOUR_CONSUMER_KEY"
      config.consumer_secret = ​"YOUR_CONSUMER_SECRET"
      config.access_token = ​"YOUR_ACCESS_TOKEN"
      config.access_token_secret = ​"YOUR_ACCESS_SECRET"
     end

In the initializer, self (that is, the Twitter::REST::Client instance) is passed into the block. Within the block body, the instance methods are called. These instance methods were created with attr_accessor. You can now try it out:

 client = Twitter::REST::Client.new ​do​ |config|
  config.consumer_key = ​"YOUR_CONSUMER_KEY"
  config.consumer_secret = ​"YOUR_CONSUMER_SECRET"
  config.access_token = ​"YOUR_ACCESS_TOKEN"
  config.access_token_secret = ​"YOUR_ACCESS_SECRET"
 end
 
 p client.consumer_key

What happens if you initialize the client without a block? You’ll get a LocalJumpError complaining that no block is given:

 'initialize': no block given (yield) (LocalJumpError)

This is an easy fix. Remember block_given?? You can use it in initialize:

 def​ initialize
 yield​ self ​if​ block_given?
 end

Run the code again, and this time everything should work as expected. We will revisit this example soon, where we consider an even more flexible approach to initializing the config object. But before that, let’s enter into the world of meta-programming and DSL creation.

Implementing a Router DSL

In the previous section, you saw one flavor of object initialization using blocks. Here’s another example, adapted and modified from Rails. Let’s imagine that you want to define a bunch of routes in a web framework, such as Rails. Routes are rules that you declare for an incoming web request. These rules invoke the appropriate controller and controller method, depending on the pattern of the URL of the incoming web request. For example, if the web server receives a request with http://localhost:3000/users, a route would parse the incoming request URL and ensure that the index method of the UsersController will be invoked.

In Rails, this file is located in config/routes.rb. In older versions of Rails, the syntax looked like this:

 routes = Router.new ​do​ |r|
  r.match ​'/about'​ => ​'home#about'
  r.match ​'/users'​ => ​'users#index'
 end

However, as Rails evolved, the way routes were defined also changed:

 routes = Router.new ​do
  match ​'/about'​ => ​'home#about'
  match ​'/users'​ => ​'users#index'
 end

In the new syntax, the block no longer expects an argument. With nothing to pass into yield, which object does match belong to? Learning the techniques in creating this variation to object instantiation will also allow you to create DSLs.

Create a file called router.rb, and fill it in with this initial implementation:

 routes = Router.new ​do
  match ​'/about'​ => ​'home#about'
  match ​'/users'​ => ​'users#index'
 end
 
 class​ Router
 # We are going to implement this!
 end

The end goal is to print out the routes:

 {"/about"=>"home#about"}
 {"/users"=>"users#index"}

An actual router implementation will parse the string and invoke the appropriate controller and method. For our purpose, printing out the routes will suffice. Your job is to fill in the body of the Router class. One way the task can potentially be simplified is to create an implementation that looks like this:

 routes = Router.new ​do​ |r|
  r.match ​'/about'​ => ​'home#about'
  r.match ​'/users'​ => ​'users#index'
 end

This should look familiar to you. Let’s go through building up this class, though this time the pace will be faster.

The initializer of Router takes a block that accepts a single argument. That argument is the object itself. You should also be able to infer that Router has an instance method called match that takes a hash as an argument. Here’s the fastest way to implement this class:

 class​ Router
 def​ initialize
 yield​ self
 end
 
 def​ match(route)
  puts route
 end
 end

Of course, the match method doesn’t do anything interesting. But that’s not the point. Here’s the challenge: how do you get from

 routes = Router.new ​do​ |r|
  r.match ​'/about'​ => ​'home#about'
  r.match ​'/users'​ => ​'users#index'
 end

to this?

 routes = Router.new ​do
  match ​'/about'​ => ​'home#about'
  match ​'/users'​ => ​'users#index'
 end

To make that leap, you will need to know about instance_eval and some meta-programming gymnastics. Let’s get to that right away.

Using instance_eval to Change self

When a method is called without a receiver (the object that the method is called on), it is assumed that the receiver is self. What does self mean in a block? Open irb and let’s find out. Let’s ask irb the oldest existential question in the world: what is self?

 >>​ self
 =>​ main
 >>​ self.class
 =>​ Object

Recall that in Ruby, everything is an object. This means that when you perform some operation, this is done within the context of some object. In this case, main is an object that belongs to the Object class.

Now, let’s answer the next question. Within the confines of a block, what does self refer to? Try this out in irb:

 >>​ ​def​ foo
 >>​ ​yield​ self
 >>​ ​end
 =>​ ​:foo
 
 >>​ foo ​do
 >>​ puts self
 >>​ ​end
 main
 =>​ ​nil

In a block, self is in the context where the block was defined. Since the block was defined in the main scope, self refers to the main. This means that doing

 routes = Router.new ​do
  match ​'/about'​ => ​'home#about'
 end

will result in an error:

 in `block in <main>': undefined method `match' for main:Object (NoMethodError)

This is because Ruby is trying to look for the match method defined on main. You need to tell Ruby to evaluate the match method in the context of Router. In less malleable languages, it is extremely difficult to accomplish this. Not with Ruby.

Changing Context with instance_eval

You need to somehow convince Ruby that the self in

 self.match ​'/about'​ => ​'home#about'

refers to the Router instance, not the main object. This is exactly what instance_eval is for. instance_eval evaluates code in the context of the instance. In other words, instance_eval changes self to point to the instance you tell it to.

Back to router.rb. Modify initialize to look like this:

 class​ Router
 
 def​ initialize(&block)
 # self is the Router instance
  instance_eval &block
 end
 
 # ...
 end

There are some new things with this code. Previously, all our encounters with block invocation were implicit via yield. Here, the code makes explicit that the block should be captured in &block. The reason it needs to be stored in a variable is because the block needs to be passed into instance_eval.

What about the "&" in &block? For that, we need to take a quick detour to learn about block-to-Proc conversion.

Block-to-Proc Conversion

We have not covered Procs yet. For now, think of them as lambdas. Blocks are not represented as objects in Ruby. However, instance_eval expects to be given an object. Therefore, we need to somehow turn a block into an object.

In Ruby, this is done via a special syntax: &block. When Ruby sees this, it internally converts the captured block into a Proc object.

The rules for block-to-Proc conversion can be utterly confusing. Here’s a useful way to remember it:

  1. Block Proc if &block is in a method argument.

  2. Proc Block if &block is in method body.

Now that you understand block-to-Proc conversion, let’s circle back to where we left off and see how everything is put together. This is the final result:

 class​ Router
 
 def​ initialize(&block)
  instance_eval &block
 end
 
 def​ match(route)
  puts route
 end
 
 end
 
 routes = Router.new ​do
  match ​'/about'​ => ​'home#about'
 end

When the Router instance is given a block, it is converted into a Proc, then passed into instance_eval. Since the context where instance_eval is invoked is Router, the Proc object is also evaluated in the Router context. This means that the match method is invoked on the Router instance.

Now that you know how instance_eval and block-to-Proc conversion works, let’s revisit an earlier example on object initialization and apply your newfound knowledge.

Object Initialization, Revisited

Let’s say you want the initializer to be slightly more flexible and take an options hash. Furthermore, you also think that specifying config.consumer_secret is too verbose. In other words, something like this:

 client = Twitter::REST::Client.new({​consumer_key: ​​"YOUR_CONSUMER_KEY"​}) ​do
  consumer_secret = ​"YOUR_CONSUMER_SECRET"
  access_token = ​"YOUR_ACCESS_TOKEN"
  access_token_secret = ​"YOUR_ACCESS_SECRET"
 end

How could this be implemented? First, the hash of options would need to be iterated through. This would then be followed by calling the block:

»def​ initialize(options = {}, &block)
» options.each { |k,v| send(​"​​#{​k​}​​="​, v) }
» instance_eval(&block) ​if​ block_given?
»end

Take note of the extra = in calling send. Because the methods were constructed using attr_accessor, Ruby uses the trailing = to indicate that the method is an attribute writer. Otherwise, it assumes it is an attribute reader.

 module​ Twitter
 module​ REST
 class​ Client
 attr_accessor​ ​:consumer_key​, ​:consumer_secret​,
 :access_token​, ​:access_token_secret
 
»def​ initialize(options = {}, &block)
» options.each { |k,v| send(​"​​#{​k​}​​="​, v) }
» instance_eval(&block) ​if​ block_given?
»end
 end
 end
 end
 
 client = Twitter::REST::Client.new({​consumer_key: ​​"YOUR_CONSUMER_KEY"​}) ​do
  consumer_secret = ​"YOUR_CONSUMER_SECRET"
  access_token = ​"YOUR_ACCESS_TOKEN"
  access_token_secret = ​"YOUR_ACCESS_SECRET"
 end
 
 p client.consumer_key ​# => YOUR_CONSUMER_KEY
 p client.access_token ​# => YOUR_ACCESS_TOKEN

Now, anyone initializing a Twitter::Rest::Client can choose to do it via an options hash, use a block, or use a combination of both. Now it’s time to flex those programming muscles and work on the exercises.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.246.245