Tapping Into The Method Chain
Compare these two solutions for returning the name of all the audiophiles in a contact book:
A first-pass underscore solution might look something like this:
The same can be achieved by chaining native methods:
The second is far cleaner, and easier to read; especially as transformations grow beyond a handful of steps. Many libraries recognise this, providing APIs that are designed to be chained; commonly called Fluent APIs. Underscore even ships its own version, allowing you to use all of underscores methods with any object, at the price of a little boilerplate:
The ordering is clear, but the intent is somewhat obscured.
Code Organisation Beyond Toy Examples
In these examples, it’s clear that method chaining, either with native array methods, or a wrapped underscore object, is up to the job of generating reasonable, concise code. The disparity between real-world use-cases and toy example is that real-world software grows as time goes on. It grows a whole lot.
Different use-cases often call for different behaviours against similar data, which leads to a proliferation of necessary moving parts; and need for reasonably concise, readable code is as great as ever. The question of where to put this behaviour, and how to share it, is one of the essential debates in the nitty-gritty of software engineering.
If we give the above transformation the name
getAudiophileNames, we can see this debate in action:
Object-Orientation would hold that the actions against the data should be shipped with the data. Adding a
getAudiophileNames method in es5 might look like this:
Note that we can compose these parts as easily as ever:
I find myself shying away from using methods as the default unit of work in my code; each essential data type has so many fundamental units of work associated with it, it would require adding methods of many different granularities to get the work done. These methods would be globally available, which would increase the complexity of working with that object type across the code-base. It also increases the risk of subtly breaking code elsewhere; especially if that code uses duck-typing to establish what kind of object it’s dealing with.
Instead, a lot of work happens at the level of fine-grained functions (pure ones, as often as I can). These functions are either generic and shared, or very specific and scoped within the area to which they specifically relate.
audiophileP are two such examples, as is the following
When used with chainable methods (such as
map), these work fantastically. However, interoperability when using functions that take the entire object is more troublesome. In most cases, it requires breaking the chain:
Tapping into the Method Chain
Using functions in this way seperates the concerns about what we’re doing and where it’s available, but it’s not nearly so easy to read. One simple, but drastic, way to improve this can be fit into just one line (in a minimal implementation):
This gives us the ability to clean up our example significantly:
Now we happily borrow functions and use them as though they were chained methods; meaning that we could write the entire transformation from front to back as an easy-to-follow chain:
Compared to the equivalent, unbroken, naive functional equivalent:
Taking extra arguments
Many generic functions require more than a single argument; so it would extend
.taps usefulness to cater to these functions:
Now there’s really no difference in flexibility between tapping a function into the method chain, and a regular method:
Trade-Offs and Dragons
However, if there ever was a good case for offending this well-honed sensibility, I think that a
At the price of one global method, we’ve basically separated the concerns of concise, readable code and the scope of a unit of of work: functions hidden or shared as needed, vs a method always tied to an object type globally.
EDIT: A Real Implementation
An implemenetion of this is available on github and npm. Notably, this implementation does not change the global object (or any others). Instead, it provides a
mixin function that adds tap to an object passed in (making the property non-enumerable if possible).