In my first post on streams, I discussed the
Transform classes and how you override them to create your own sources, sinks and filters.
However, where Node.js streams diverge from more classical models (e.g., from the shell), is Object streams. Each of the three types of stream objects can work with objects (instead of buffers of bytes) by passing the
objectMode parameter set to
true into the parent class constructor’s options argument. From that point on, the stream will deal with individual objects (instead of groups of bytes) as the medium of the stream.
This has a few direct consequences:
Readableobjects are expected to call
pushonce per object, and each argument is treated as a new element in the stream.
Writeableobjects will receive a single object at a time as the first argument to their
_writemethods, and the method will be called once for each object in the stream.
Transformobjects have the same changes as the both the other two objects.
Application: Tax Calculations
At first glance, it may not be obvious why object streams are so useful. Let me provide a few examples to show why. For the first example, consider performing tax calculations for a meal in a restaurant. There are a number of different steps, and the outcome for each step often depends upon the results of another. The whole thing can get very complex. Object streams can be used to break things down into manageable pieces.
Let’s simplify a bit and say the steps are:
- Apply item-level discounts (e.g., mark the price of a free dessert as $0)
- Compute the tax for each item
- Compute the subtotal by summing the price of each item
- Compute the tax total by summing the tax of each item
- Apply check-level discounts (e.g., a 10% discount for poor service)
- Add any automatic gratuity for a large party
- Compute the grand total by summing the subtotal, tax total, and auto-gratuity
Of course, bear in mind that I’m actually leaving out a lot of detail and subtlety here, but I’m sure you get the idea.
You could, of course, write all this in a single big function, but that would be some pretty complicated code, easy to get wrong, and hard to test. Instead, let’s consider how you might do the same thing with object streams.
First, let’s say we have a
Readable which knows how to read orders from a database. It’s constructor is given a connection object of some kind, and the order ID. The
_read method, of course, uses these to build an object which represents the order in memory. This object is then given as an argument to the
Next, let’s say each of the calculation steps above is separated into its own
Transform object. Each one will receive the object created by the
Readable, and will modify it my adding on the extra data it’s responsible for. So, for example, the second transform might look for an
items array on the object, and then loop through it adding a
taxTotal property with the appropriate computed value for each item. It would then call its own
push method, passing along the primary object for the next
After having passed from one
Transform to the next, the order object created by the
Readable would wind up with all the proper computations having been tacked on, piece-by-piece, by each object. Finally, the object would be passed to a
Writable subclass which would store all the new data back into the database.
Now that each step is nicely isolated with a very clear and simple interface (i.e., pass an object, get one back), it’s very easy to test each part of the calculation in isolation, or to add in new steps as needed.