Reuse code with domain-specific steps in collection pipelines
How being mindful of collection processing promotes code reuse
Why extend pipelines
So far, we’ve only looked into how to add new functions to collection pipelines. In this post, we’ll look into why such extensions might be necessary.
We’ve used a rather simple
head function as an example, which raises the obvious question: What if a library provides all the fundamental steps? In that case, extending the pipeline with new functions is a non-issue, and all the problems we’ve talked about previously are non-existent.
If you only want to use generic, building-block type functions, this might be true. In that case, choose a library that provides all the functions you need.
But for complex apps, functions that are specific to the domain might be needed to promote code reuse. In that case, no library can provide them out of the box.
This post argues that even though libraries can provide all the necessary building blocks for collection processing, being conscious about extending the pipelines promotes code reuse and thus a cleaner architecture.
In simple cases, moving the iteratee to a separate function is enough.
As an example, filter an array of users by some non-trivial condition (Try it):
If you need this filtering in many places, refactor it to a function (Try it):
Extending the pipeline
The problem is when the change itself does not fit into a
map, or the other functions that are provided by the pipeline.
For example, filter the non-active users and also assign an
activity rating to the objects. This operation is a
filter, followed by a
map, so it does not fit into either of them (Try it):
Moving this to a separate function is usually done with a
([users]) => [users] signature (Try it):
But in this case, this function does no longer fit into the pipeline:
Instead of a flat structure, we’re back to square one.
Using functional composition
Extending Arrays is an anti-pattern, as we’ve seen in a previous post in this series. Implementing and using chaining comes with its own problems.
But function composition offers a solution.
As a recap, we have a
flow function that operates similar to the UNIX pipe, composing the argument functions. And the
filter functions are partially applied, i.e., they get the iteratee on the first call and the collection itself on the second one.
The above example, rewritten to use functional composition (Try it):
To move this to a reusable function, just extract the pipeline itself:
This value is itself a valid pipeline, and can be part of a larger one:
This way, arbitrarily deep pipelines are possible, without losing the benefits. Parts used in multiple places can be moved to a central location, promoting code reuse.
ImmutableJs provides a function called
update that can be used to achieve the same effect. It gets the whole collection and returns a new one (Try it):
update, ImmutableJs collections are easy to extend with domain-specific steps.
Extending a collection pipeline with custom processing steps is an everyday task in complex apps. Doing it right promotes code reuse.
The most important step is to know the libraries. If they provide a way of adding custom steps, like ImmutableJs does, use that. If not, use functional composition instead of Array#Extras for any non-trivial processing.