Refactoring from functional to prototypical

The functional mock-up created in the previous recipe can be useful for gaining mental traction with a concept (that is, getting our head around it), and may be perfectly adequate for small, simple modules with narrow scope.

However, the prototype pattern (among others) is commonly used by module creators, often used in Node's core modules and is fundamental to native JavaScript methods and objects.

Prototypical inheritance is marginally more memory efficient. Methods sitting on a prototype are not instantiated until called, and they're reused instead of recreated on each invocation.

On the other hand, it can be slightly slower than our previous recipe's procedural style because the JavaScript engine has the added overhead of traversing prototype chains. Nevertheless, it's (arguably) more appropriate to think of and implement modules as entities in their own right, which a user can create instances of (for example, a prototype-oriented approach). For one, it makes them easier to programmatically extend through cloning and prototype modifications. This leads to great flexibility being afforded to the end user while the core integrity of the module's code stays intact.

In this recipe, we'll rewrite our code from the previous task according to the prototype pattern.

Getting ready

Let's start editing index.js in mp3dat/lib.

How to do it...

To begin, we'll need to create a constructor function (a function called using new), which we'll name Mp3dat:

var fs = require('fs'),

function Mp3dat(f, size) {
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat(f, size);
  }
  this.stats = {duration:{}};
}

We've also required the fs module as in the previous task.

Let's add some objects and methods to our constructor's prototype:

Mp3dat.prototype._bitrates = { 1 : 32000, 2 : 40000, 3 : 48000, 4 : 56000, 5 : 64000, 6 : 80000, 7 : 96000, 8 : 112000, 9 : 128000, A : 160000, B : 192000, C : 224000, D : 256000, E : 320000 };
   
Mp3dat.prototype._magnitudes = [ 'hours', 'minutes', 'seconds', 'milliseconds'];

Mp3dat.prototype._pad = function (n) { return n < 10 ? '0' + n : n; }  

Mp3dat.prototype._timesig = function () {
  var ts = '', self = this;;
  self._magnitudes.forEach(function (mag, i) {
   if (i < 3) {
    ts += self._pad(self.stats.duration[mag]) + ((i < 2) ? ':' : ''),
   }
  });
  return ts;
}

Three of our new Mp3dat properties (_magnitudes, _pad, and _timesig) were contained in the buildStats function in some form. We've prefixed their names with the underscore (_) to signify that they are private. This is merely a convention, JavaScript doesn't actually privatize them.

Now we'll reincarnate the previous recipe's findBitRate function as follows:

Mp3dat.prototype._findBitRate = function(cb) {
  var self = this;
   fs.createReadStream(self.f)
    .on('data', function (data) {
      var i = 0;
       for (i; i < data.length; i += 2) {
        if (data.readUInt16LE(i) === 64511) {
          self.bitrate = self._bitrates[data.toString('hex', i + 2, i + 3)[0]];
          this.destroy();
          cb(null);
          break;
        };
    }
  }).on('end', function () {
    cb(new Error('could not find bitrate, is this definitely an MPEG-1 MP3?'));
  });
}

The only differences here are that we load the filename from the object (self.f) instead of via the first parameter, and we load bitrate onto the object instead of sending it through the second parameter of cb.

Now to convert buildStats into the prototype pattern, we write the following code:

Mp3dat.prototype._buildStats = function (cb) {
  var self = this,
  hours = (self.size / (self.bitrate / 8) / 3600);

  self._timeProcessor(hours, function (duration) {
    self.stats = {
      duration: duration,
      bitrate: self.bitrate,
      filesize: self.size,
      timestamp: Math.round(hours * 3600000),
      timesig: self._timesig(duration, self.magnitudes)
    };
    cb(null, self.stats);
    
  });
}

Our _buildStats prototype method is significantly smaller than its buildStats cousin from the previous task. Not only have we pulled its internal magnitudes array, pad utility function, and time signature functionality (wrapping it into its own _timesig method), we've also outsourced the internal recursive timeProcessor function to a prototype method equivalent.

Mp3dat.prototype._timeProcessor = function (time, counter, cb) {
  var self = this, timeArray = [], factor = (counter < 3) ? 60 : 1000,
    magnitudes = self._magnitudes, duration = self.stats.duration;
    
  if (counter instanceof Function) {
    cb = counter;
    counter = 0;
  }

  if (counter) {        
    timeArray = (factor * +('0.' + time)).toString().split('.'),
  }
  if (counter < magnitudes.length - 1) {
    duration[magnitudes[counter]] = timeArray[0] || Math.floor(time);
    duration[magnitudes[counter]] = +duration[magnitudes[counter]];
    counter += 1;
    self._timeProcessor.call(self, timeArray[1] || time.toString().split('.')[1], counter, cb);
    return;
  }
    //round off the final magnitude (milliseconds)
    duration[magnitudes[counter]] = Math.round(timeArray.join('.'));
    cb(duration);
}

Finally, we write the stat method (with no underscore prefix since it's intended for public use), and export the Mp3dat object.

Mp3dat.prototype.stat = function (f, cb) {
  var self = this;
  fs.stat(f, function (err, fstats) {
    self.size = fstats.size;
    self.f = f;
    self._findBitRate(function (err, bitrate) {
      if (err) { cb(err); return; }
      self._buildStats(cb);
    });    
  });
}

module.exports = Mp3dat();

We can ensure all is present and correct by running the tests we built in the first recipe. On the command line from the mp3dat folder we say:

node test

Which should output:

All tests passed

How it works...

In the previous recipe, we had an exports.stat function which called the findBitRate and buildStats functions to get the stats object. In our refactored module, we add the stat method onto the prototype and export the entire Mp3dat constructor function via module.exports.

We don't have to pass Mp3dat to module.exports using new. Our function generates the new instance when invoked directly, with the following code:

  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }

This is really a failsafe strategy. It's more efficient (though marginally) to initialize the constructor with new.

The stat method in our refactored code differs from the exports.stat function in the prior task. Instead of passing the filename and size of the specified MP3 as parameters to findBitRate and buildStats respectively, it assigns them to the parent object via this (which is assigned to self to avoid new callbacks scopes reassignment of this).

It then invokes the _findBitRate and _buildStats methods to ultimately generate the stats object and pass it back to the users callback.

After running mp3dat.stats on our test.mp3 file, our refactored mp3dat module object will hold the following:

{ stats:
   { duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
     bitrate: 128000,
     filesize: 82969,
     timestamp: 5186,
     timesig: '00:00:05' },
  size: 82969,
  f: 'test/test.mp3',
  bitrate: 128000 }

In the former recipe however, the returned object would simply be as follows:

{ stat: [Function] }

The functional style reveals the API. Our refactored code allows the user to interact with the information in multiple ways (through the stats and mp3dat objects). We can also extend our module and populate mp3dat with other properties later on, outside of the stats object.

There's more...

We can structure our module to make it even easier to use.

Adding the stat function to the initialized mp3dat object

If we want to expose our stat function directly to the mp3dat object, thus allowing us to view the API directly (for example, with console.log), we can add it by removing Mp3dat.prototype.stat and altering Mp3dat as follows:

function Mp3dat() {
  var self = this;
  if (!(this instanceof Mp3dat)) {
    return new Mp3dat();
  }
  self.stat = function (f, cb) {
    fs.stat(f, function (err, fstats) {
      self.size = fstats.size;
      self.f = f;
      self._findBitRate(function (err, bitrate) {
        if (err) { cb(err); return; }
        self._buildStats(cb);
      });    
    });
  }  
  self.stats = {duration:{}};
}

Then our final object becomes:

{ stat: [Function],
  stats:
   { duration: { hours: 0, minutes: 0, seconds: 5, milliseconds: 186 },
     bitrate: 128000,
     filesize: 82969,
     timestamp: 5186,
     timesig: '00:00:05' },
  size: 82969,
  f: 'test/test.mp3',
  bitrate: 128000 }

Alternatively, if we're not concerned about pushing the stats object and other Mp3dat properties through to the module user, we can leave everything as it is, except change the following code:

module.exports = Mp3dat()

To:

exports.stat = function (f, cb) {
  var m = Mp3dat();
  return Mp3dat.prototype.stat.call(m, f, cb);
}

This uses the call method to apply the Mp3dat scope to the stat method (allowing us to piggyback off of the stat method) and will return an object with the following:

{ stat: [Function] }

Just as in the first write of our module, except we still have the prototype pattern in place. This second approach is ever so slightly more efficient.

Allowing for multiple instances

Our module is a singleton since returns the already initialized Mp3dat object. This means no matter how many times we require it and assign it to variables, a module user will always be referring to the same object, even if Mp3dat is required in different submodules loaded by a parent script.

This means bad things will happen if we try to run two mp3dat.stat methods at the same time. In a situation where our module is required multiple times, two variables holding the same object could end up overwriting each other's properties, resulting in unpredictable (and frustrating) code. The most likely upshot is that readStreams will clash.

One way to overcome this is to alter the following:

module.exports = Mp3dat()

To:

module.exports = Mp3dat

And then load two instances with the following code:

var Mp3dat = require('../index.js'),
	mp3dat = Mp3dat(),
      mp3dat2 = Mp3dat();

If we wanted to provide both singletons and multiple instances, we could add a spawnInstance method to our constructor's prototype:

Mp3dat.prototype.spawnInstance = function () {
  return Mp3dat();
}

module.exports = Mp3dat();

Which then allows us to do something as follows:

var mp3dat = require('../index.js'),
   mp3dat2 = mp3dat.spawnInstance();

Both mp3dat and mp3dat2 would be separate Mp3dat instances, whereas in the following case:

var mp3dat = require('../index.js'),
   mp3dat2 = require('../index.js'),

Both would be the same instance.

See also

  • Writing a functional module mock-up discussed in this chapter
  • Extending the module's API discussed in this chapter
  • Deploying a module to npm discussed in this chapter
..................Content has been hidden....................

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