Taking Baby Steps with Node.js – The Towering Inferno

April 20th, 2011

Here are the links to the previous installments:

  1. Introduction
  2. Threads vs. Events
  3. Using Non-Standard Modules
  4. Debugging with node-inspector
  5. CommonJS and Creating Custom Modules
  6. Node Version Management with n
  7. Implementing Events
  8. BDD Style Unit Tests with Jasmine-Node Sprinkled With Some Should
  9. “node_modules” Folders
  10. Pumping Data Between Streams
  11. Some Node.js Goodies

Because Node.js puts an event-based model at its core, it kindly guides you towards executing all I/O operations asynchronously. As discussed in previous posts, JavaScript perfectly fits this purpose by enabling you to provide a callback that gets executed when a particular I/O operation has finished doing its task. This is all great, but it also involves a mental burden, regarding the readability and maintainability of JavaScript code that can get out of control very quickly. Let’s discuss this further over some code.

http.createServer(function(request, response) {
    var podcastsFilePath =  path.join(__dirname, 'podcasts.txt');
        
    fileSystem.readFile(podcastsFilePath, 'utf8', function(error, originalData) {
        if(error) 
            throw error;
        
        if(-1 == originalData.indexOf('Astronomy Podcast'))    {        
            var favoritePodcasts = originalData + '\n' + 'Astronomy Podcast';    
            fileSystem.writeFile(podcastsFilePath, favoritePodcasts, function(err) {
                if(error) 
                    throw error;
                
                writeResponse(favoritePodcasts);    
            });    
        }
        else {
            writeResponse(originalData);        
        }
    });        
    
    function writeResponse(responseData) {
        response.writeHead(200, {
                'Content-Type': 'text/html', 
                'Content-Length': responseData.length
            });
            
        response.end(responseData);                    
    }
})
.listen(2000);

Here we create a simple HTTP server that reads in a text file when it receives a request. This file simply contains the names of all the podcasts that I personally enjoy listening to. After the content of the file is retrieved, we check whether the Astronomy Podcast is part of the list (yes, it’s that good :-) ). If it’s not in there, the Astronomy Podcast is added to the list of favorite podcasts and the result is written back to the text file. The request is then completed by returning the entire contents of list in the response.

Have a look at the readability of the code that implements this simple example. It’s not that great, if you ask me. And the reason for that is the amount of nested callbacks.

image

These nested callbacks create some sort of horizontal tower effect which makes the event-based approach a bit more cumbersome compared to more traditional programming. But the good news is that we’re able to improve this code by making use of the step library.

Step is a simple control-flow library for Node.js that makes serial execution, parallel execution, and error handling seamless and less painful. The step library simply provides a single function that on its turn accepts any number of functions that are executed in series and in the order that they are provided. Every step in the sequence also knows about the outcome of the previous function.

Let’s take a look at the refactored code for our simple example.

http.createServer(function(request, response) {
    step(
        function assembleFilePath() {
            podcastsFilePath =  path.join(__dirname, 'podcasts.txt');        
            // Because there's no callback needed for this step, 
            // we have to manually call:
            this();        
        },
        
        function readFavoritePodcastsFromFile() {
            fileSystem.readFile(podcastsFilePath, 'utf8', this);            
        },
        
        function addNewFavoritePodcastToFile(err, originalData) {
            if(-1 == originalData.indexOf('Astronomy Podcast'))    {
                favoritePodcasts = originalData + '\n' + 'Astronomy Podcast';    
                fileSystem.writeFile(podcastsFilePath, favoritePodcasts, this);            
            }
            else {
                favoritePodcasts = originalData;
                this();    
            }
        },
        
        function writeResponse() {
            response.writeHead(200, {
                'Content-Type': 'text/html', 
                 'Content-Length': favoritePodcasts.length
            });
            
            response.end(favoritePodcasts);        
        }
    );
})
.listen(2000);

Now this reads a lot nicer, don’t you think?

Using the step library, we were able to refactor the nested callbacks from our first implementation into separate functions that make up a sequence. In the first function we assemble the path for the text file. However, this code doesn’t require a callback, so we need to manually call this() in order to get to the next step in the sequence. I made this an explicit step to show that it’s not required to have a callback for a particular step.

For the second function, we read in the content of the text file. Notice that instead of providing a callback to the readFile() function, we simply provide this.

The third function checks whether the Astronomy Podcast is part of the favorites and if not, the results are written back to the text file. Notice that the parameters of the addNewFavoritePodcastToFile function are exactly the same parameters for the callback that we provided for the readFile() function in our first implementation. This is definitely not a coincidence as the step library ensures that the outcome for the callback of a particular step is provided as input for the next step.

The last function in our sequence simply adds the list to the response object. Also notice that we have the possibility to store data in variables that are accessible in every step of the sequence (e.g. the podcastsFilePath variable in the first function).

Although, executing functions in series is the default, the step library also enables us to execute operations in parallel.

step(
      // Loads two files in parallel
      function loadStuff() {
          fileSystem.readFile(__filename, this.parallel());
          fileSystem.readFile("/etc/passwd", this.parallel());
      },
      // Show the result when done
      function showStuff(error, code, users) {
          if (error) throw error;
          
          console.log(code);
          console.log(users);
      }
);

 

Using the step library we’re really able to take baby steps with Node.js ;-). Until next time.

  • http://www.bbleo.com/ love stories

    thank you for the great topic