Modularity first started looking really interesting to me after I read the legendary SICP book. It surprised me to learn that for some people, making code understandable is so important that they write their own little languages just to make the program readable to anyone (the book also teaches you how to build an interpreter, an encoder, and other cool things).
Although SICP gives you a solid introduction to the art of modularity, it’s still hard to create your own vocabulary if you’re not using Lisp. For other languages, we have to get creative to be able to encapsulate code into bite-sized chunks.
Why and how to write modular code
Modularity exists entirely because of the human side of development. A computer doesn’t need a broken-down and embellished version of the code to be able to run it. It was our cognitive limitations that got us to write code in smaller pieces at a time.
A particular problem where this becomes obvious is when dealing with temporary contexts. For example, say you’re writing to a file but a condition arises where you need to write to another file. You need to temporarily forget about the first file and all of its related data to deal with the second one instead.
This kind of situation was confusing and overwhelming, so functions were invented. A typical way to handle this now is by making a function that writes arbitrary data to any file, to isolate the temporary context inside it.
What happens if the context is actually very small? Even a function might be overkill. Another way to handle temporary context is by putting logic inside variables. Taking advantage of the fact that logical expressions get reduced to a boolean value, most languages will let you do this.
We know that these small modularization ideas are beneficial when we can appreciate that code starts to read like a story, more than a group of disconnected formulas. When you make changes to your code, ask yourself: does this change make the implementation’s logic more evident or does it make it inscrutable?
Sometimes when you’re writing a function you realize that some of the code looks repetitive, so you decide to create another function. Most languages would require you to make the new function under the same namespace. This is where JS nested functions come in handy. You can place a function inside another one, so that the internal one can only be accessed from your original method.
An interesting property of this technique comes up when you use IDEs that have code folding. They allow you to fold the external and internal functions together in this case, which is much faster than folding individual ones.
These come in handy when you want to encapsulate data access to the smallest part of the code that you can. Any change you make to your code is bound to make ripples through your program. If you modify a lot of code, there will likely be a strong impact on the codebase. For a larger codebase, even a small change could have a large impact.
That’s why these features let you avoid bugs resulting from accidental data modifications. Block-scoped variables localize the impact of changes.
Although block scopes can let you reuse variable names, that’s not what they are most useful for. The real benefit comes from not being able to access the internal data. You’re creating a safety net so that when a bug comes up, you know that you can ignore internal variables if the problem is outside the block.
Identifying new ways to separate concerns in your code is important, because it lets your mind worry about less things when debugging. You should aim to keep a high cohesion and encapsulation, and having as many tools as possible to do that will make you more productive. Once you are used to focusing on making your code tight, you will have less bugs to worry about.
Cohesion and coupling are really important concepts for writing clean code. Fortunately, there are already many good articles about them. Here’s a great introduction to cohesion. If you’re not familiar with these ideas, the article is somewhat technical, but definitely worth studying.
Modularity sometimes causes more problems than it solves
After you begin to appreciate the value of breaking ideas down into smaller pieces, there’s a risk that you’ll see it as the silver bullet of clean code. You wouldn’t be the first.
Modularity is no panacea. Let me show you some examples where it causes more problems than it solves.
Programming using interfaces (contracts) is a very powerful idea. Taking advantage of that, some frameworks bring with them a whole set of interchangeable classes. For example, to handle persistence there might be several classes implementing the persistence interface. Unfortunately, IDEs get confused by this, so while reading code you will try to find the source of a method, and the IDE will not know which method you want to see, so it will show you a long list of files where a method with that name exists. It can be a real pain to have to sift through that list every time.
There are tons of JS modules out there that include just a single small function. Each module requires additional parsing and processing time, and includes their own header in the code, so using a lot of small modules will add overhead to your build system and increase your bundle size.
Centralization of dependencies
Excessive modularity has also caused problems due to the centralization of code, which happens once enough people start using a module. That was the case with left-pad, which was brought down, taking many projects with it for a few days.
Refactoring for the sake of it
Some code almost never changes. In those cases it might not make sense to try to make it look cleaner or to abstract logic that is only used there and that already works well. The other day I was reading the Redis code and a method stood out, “loadServerConfigFromString”. I thought, this code doesn’t look too pretty and yet, according to git blame, it has not changed much in the last 7 years. There is no reason to refactor code that never changes and already works fine.
One of my core philosophies when working with technical issues is always being aware that the implementation details alone will make or break an idea. Always be aware of where and how you are going to apply modularity, and how it affects the development environment.
Posted on August 23, 2017