Block Pattern #2: Managing Resources

Blocks are an excellent way to abstract pre- and post-processing. A wonderful example of that is resource management. Examples of resources that require extra care include file handles, socket connections, and database connections. For example, failure to close a database connection means that down the line, another connection attempt might be refused, since the number of connections that a database can handle is finite and limited.

Remembering to open and close the resource is a largely manual affair. This is error-prone and requires a bit of boilerplate. In the following example, the programmer is trying to open a file and write a few lines to it. The last line is where the programmer closes the file handle:

 f = File.open(​'Leo Tolstoy - War and Peace.txt'​, ​'w'​)
 f << ​"Well, Prince, so Genoa and Lucca"
 f << ​" are now just family estates of the Buonapartes."
 f.close

What happens if the programmer forgets to close the file with f.close? The severity depends on how long the program runs. If this code were to be part of a one-off script, then the situation wouldn’t be that bad. The file handle would be terminated once the script finished execution. But if you have a long-running application like a daemon or web application, then this is bad news. That’s because the operating system can only support a finite number of file handles. If the long-running daemon continuously opens files and doesn’t close them, soon enough the file handles will run out, and you’ll get a call or page in the middle of the night. In other words, you have a resource leak on your hands.

If you think about it, the only thing we really want is to write to the file. Having to remember to close the file handle is a hassle. Ruby has a very elegant way of doing this, using blocks:

 File.open(​'Leo Tolstoy - War and Peace.txt'​, ​'w'​) ​do​ |f|
  f << ​"Well, Prince, so Genoa and Lucca"
  f << ​" are now just family estates of the Buonapartes."
 end

By passing in a block into File.open, Ruby helps you, the over-burdened (and downright lazy) developer, to close the file handle when you’re done writing the program. Notice that the file handle is nicely scoped within the block. In other words, f only exists within the confines of the block. But where exactly is the file closing taking place? Let’s find out.

Implementing File.open

Let’s unravel the mysteries of File.open. First of all, the Ruby documentation[3] provides an excellent overview of File.open. If you read carefully, it even provides hints of how it is implemented:

With no associated block, File.open is a synonym for ::new. If the optional code block is given, it will be passed the opened file as an argument, and the File object will automatically be closed when the block terminates. The value of the block will be returned from File.open.

This description alone is enough to kickstart our File.open implementation. Create file_open.rb and follow along.

  1. If no block is given, File.open is the same as File.new:

     class​ File
     def​ self.open(name, mode)
     new​(name, mode) ​unless​ block_given?
     end
     end
  2. If there’s a block, the block is then passed the opened file as an argument...

     class​ File
     def​ self.open(name, mode)
      file = ​new​(name, mode)
     return​ file ​unless​ block_given?
    »yield​(file)
     end
     end
  3. ...and the file is automatically closed when the block terminates...

     class​ File
     def​ self.open(name, mode)
      file = ​new​(name, mode)
     return​ file ​unless​ block_given?
     yield​(file)
    » file.close
     end
     end

    There’s a gotcha here. What happens if an exception is raised in the block? file.close will not be called, which defeats the whole point of this exercise. Thankfully, this is an easy fix with the ensure keyword:

     class​ File
     def​ self.open(name, mode)
      file = ​new​(name, mode)
     return​ file ​unless​ block_given?
     yield​(file)
    »ensure
    » file.close
    »end
     end

    Now, file.close is always guaranteed to close properly.

  4. The value of the block will be returned from File.open.

    Since yield(file) is the last line, the value of the block will be returned from File.open.

    images/blocks/block_patterns_-_resource_management/block_patterns_-_resource_management_001.jpg

    Let’s see if this works. Open file_open.rb in irb:

     % irb -r ./file_open.rb

    Let’s get meta and open the file you just opened:

     >> File.open(​"file_open.rb"​, ​"r"​) ​do​ |f|
     >> puts f.path
     >> puts f.ctime
     >> puts f.size
     >> ​end
     file_open.rb
     2016-11-13 08:32:24 +0800
     238
     => ​nil

With a little bit of work, File.open frees you from having to remember to close file handles, handles exceptional cases, and to top it off, lets you do this in a simple and beautiful API. Speaking of beautiful, blocks are also great for object initialization, as you will soon see in the next section.

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

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