Why Small Modules Matter

Big projects are what, if not the composition of small pieces

Fagner Brack
4 min readApr 29, 2016
USS Missouri, a big and complex warship built using lego pieces

Take a look at the following code:

A code example representing a node module to flatten an array inside a file

It looks for a file that contains a JSON Array inside, flattens it, and then writes again to the file system. After everything is done, it prints Done! into the console.

Take a closer look at that code. Is it possible to spot some patterns there? Is it possible to reduce the amount of cognitive overload on this piece of functionality?

To understand what can be abstracted and what cannot, we need to stop thinking about the implementation details and start thinking about the goal in an operation whose responsibility is to "flatten an array inside a file":

  1. Read the file
  2. Convert String to Array
  3. Flatten the Array
  4. Convert the flattened Array to String
  5. Write to the file

1. Read the file

The only thing we need here is readFile(fileName), and that is it. If we have a readFile function, we can abstract away all the concerns of how to read that file, including character decoding and Promises:

A code example representing a node module to read a file

On the client, it will be as simple as a readFile(fileName) call. All concerns of how to implement Promises are gone. All concerns about character encoding are gone. And when you want to test it, you don't even need to touch the file system; you can replace the readFile dependency for something else.

2. Convert JSON String to Array Literal

For the example in this post, assume we only have to work with files containing text.

Now we need to convert the string from the file to an Array Literal. We could perfectly use the JSON.parse function to do the work for us inside the flattenArrayInsideFile(fileName) function. However, is it the flatten function's responsibility to parse the return of readFile(name)?

It doesn't make sense to change the readFile(fileName) function. It has one responsibility and one responsibility only: to read a text file by decoding the contents using a standard format (in this case, UTF-8). Suppose you change readFile(fileName) to return a different type other than a String, like an Object Literal. In that case, the function won't be applicable for files that contain text data which you can't convert to JSON, such as CSV.

The solution, in this case, would be to read the file as JSON, or more specifically, readFileAsJSON(fileName):

A code example representing a node module to read a file as JSON

3. Flatten the Array

Flattening the array is the core behaviour that we are trying to achieve; everything else is infrastructure. Because it is the core functionality, it makes sense to do in a single function. But just because the function should have the knowledge to flatten, it doesn't mean that the whole algorithm to get read/write from files should be implemented there.

Flattening an array is a pretty common task. It makes sense abstracting this functionality out. The first example is doing precisely that by using the flatten-array module, that is available on npm. It's one of the modules from the "left-pad" incident that I ended up becoming custodian.

4. Convert the flattened Array to String

Here comes another exciting part. In the first example, we are using the following code to convert the flattened Array into a stringified representation:

A code example representing a call to JSON "stringify" function passing a "flattened array" variable as an argument

That's fine, except it's not the primary function's responsibility to convert anything. It should only be concerned about flattening the contents of the file, and that is it.

In this case, we should take the same approach we took for the readFileAsJSON(fileName) function and delegate the responsibility of formatting the content that is JSON-aware so that it is written correctly to the output. We need a writeFileToJSON(fileName, content):

A code example representing a node module to write a file to JSON

5. Write to the file

In the previous example, the function is doing more than just converting the content to a stringified representation. It is also concerned with the "promisification" of the task that writes the file to disk. The solution to reducing the responsibility would be to create a function whose sole concern is to write the content to disk and use that instead, the same way we are doing for reading the file:

A code example representing a node module to write a file

This way, we can reuse the functionality without having to promisify it all the time. More importantly, it makes it easier to stub the default write operation in the writeFileToJSON(fileName, content) for unit tests. Here, you don't have to be concerned with the file system, which makes us follow the principle of not mocking objects you don't own.

By separating the concerns and creating more efficient abstractions, we can build a few functions that are entirely decoupled and testable. This way, we don't need to spend much time when making changes to the domain.

Design an application composed using small functional modules is one step closer to becoming able to change any part of the domain without unintended side effects.

Here's the resulting code:

A code example representing a node module consuming the other modules created so far.

And the usage is as simple as this:

A code example showing how to use the refactored "flatten array inside file" function

You can probably over-engineer or under-engineer this code even further. From writing everything in your app, or use a module for every function published by somebody else and fetch from the network by default.

Don't get me wrong. That's not the point.

It is not about lines of code. It is not about one line modules published on NPM. It is not even about reusability, DRY or WET. It is all about thinking on efficient and useful abstractions that can make your code easier to reason about and test. Thinking about the design, cohesion and coupling, is more important than mindlessly splitting functions as you feel like.

The subject of software design is too extensive to summarize in a single post. However, I hope that this has given you enough fuel to propel into the right direction.

Thanks for reading. If you have some feedback, reach out to me on Twitter, Facebook or Github.

--

--

Fagner Brack

I believe ideas should be open and free (as in Freedom). This is a non-profit initiative to write about challenging stuff you won’t find anywhere else.