In my first post on streams, I discussed the Readable
, Writable
and 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:
Readable
objects are expected to callpush
once per object, and each argument is treated as a new element in the stream.Writeable
objects will receive a single object at a time as the first argument to their_write
methods, and the method will be called once for each object in the stream.Transform
objects 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 push
method.
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 Transform
.
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.