Modularity Principles

Modularity can be the answer to complexity, but what exactly do we mean when we’re talking about complexity?

Complexity is a loaded term for a nuanced topic. What does complex mean? A dictionary defines complex[1] as something that’s "composed of many interconnected parts", but that’s not the problem we generally refer to when we speak of complexity in the context of programming: a program may have hundreds or thousands of files and still be considered relatively simple.

The next two definitions, offered by that same dictionary, might be more revealing in the context of program design.

  • "characterized by a very complicated or involved arrangement of parts, units, etc."

  • "so complicated or intricate as to be hard to understand or deal with"

The first indicates a program can become complex when its parts are arranged in a complicated manner, i.e: the interconnections among parts become a pain point. This could stem from convoluted interfaces or lacking documentation, and it’s one of the aspects of complexity that we’ll tackle in this book.

We can interpret the second definition as the other side of the complexity coin. Components so complicated that their implementation is hard to understand, debug, or extend. Most of the book is devoted to counterbalancing and avoiding this aspect of complexity.

In broad terms, something is complex when it becomes hard to grasp or fully understand. By that definition, anything in a typical program can be complex: a block of code, a single statement, the API layer, its documentation, tests, the directory structure, coding conventions, or even a variable’s name.

Measuring complexity by lines of code proves to be trite: a file with thousands of lines of code can be simple if it’s just a list of constants like country codes or action types. Conversely, a file with a couple dozen lines of code could be insurmountably complex, not only in its interface but particularly in its implementation. Add together a few complex components and, soon, you’ll want nothing to do with the codebase.

Cyclomatic complexity is the amount of unique code paths a program can take, and it may be a better metric when measuring how complex a component is. Cyclomatic complexity only allows us to measure how complex a component has become. On its own, however, tracking this metric does little to significantly reduce complexity across our codebase or improve our coding style.

We must acknowledge that codebases are not fixed in time. Codebases typically grow along with time, much like the products we build with them. There is no such thing as a finished product or the perfect codebase. We should develop application architecture that embraces the passage of time through the ability to adjust to new conditions.

A significant body of changes to an implementation should be able to leave the API in front of said implementation unmodified. It should be possible to extend the API surface of a component with ease, and ironing out outdated API wrinkles shouldn’t be fraught with confusion or frustration. When we want to horizontally scale our program beyond single components, it should be straightforward instead of having to modify several existing components in order to accomodate each new one. How can modular design help us manage complexity both at the component level and at scale?