Modularity first started looking really interesting to me after I read the legendary SICP book (Structure and Interpretation of Computer Programs). 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, you have to get creative to be able to encapsulate code into bite-size chunks.
There are a few modular coding techniques that I use in my day-to-day work that make my code a lot more organized and maintainable. Here's why modularity is important, and useful techniques for writing modular code.
Why does modularity exist?
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's our own cognitive limitations that force us to write code in smaller pieces.
A particular area where this becomes obvious is in 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.
But what happens if the context is actually very small? Even a function might be overkill. This brings me to my first technique. All of my examples are in JavaScript, but the principles are applicable almost anywhere.
Using variables to isolate logical expressions
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.
These small modularization ideas are beneficial because you start to notice that this code reads like a story, not 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 less comprehensible?Using nested functions to encapsulate blocks of code
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 nested JavaScript 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.Preventing data access with block-scoped variables
Some languages, such as C, C++, and more recently JavaScript (ES6), let you define variables that are only accessible within a block. That means that you can’t accidentally access that variable later or in another iteration of the same loop.
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 heavy 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.Why should I apply this stuff?
Identifying new ways to separate concerns in your code is important because it allows your brain to focus on fewer things when debugging. You should aim to keep the cohesion and encapsulation in your code high, and having as many tools and techniques as possible to do that will make you more productive. Once you are used to focusing on making your code tight, you will have fewer bugs to worry about.
Cohesion and coupling are really important concepts for writing clean code. Fortunately, there are already many good articles about them. 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.
Excessive object-oriented structure
Programming that uses interfaces (contracts) is a very powerful idea. To take 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. You might be trying 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.
Modules that are too small
There are tons of JavaScript modules out there that include just a single small function. Each module requires additional parsing and processing time and includes its 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 took down many projects that had it as a dependency in their code.
Refactoring for no reason
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 already works well. The other day I was reading the Redis source 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 seven years." There is no reason to refactor code that never changes and already works fine.
Be aware of the where and how
One of my core philosophies when working with technical issues is 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.
Keep learning
Take a deep dive into the state of quality with TechBeacon's Guide. Plus: Download the free World Quality Report 2022-23.
Put performance engineering into practice with these top 10 performance engineering techniques that work.
Find to tools you need with TechBeacon's Buyer's Guide for Selecting Software Test Automation Tools.
Discover best practices for reducing software defects with TechBeacon's Guide.
- Take your testing career to the next level. TechBeacon's Careers Topic Center provides expert advice to prepare you for your next move.