In Defence of Simplicity

In his essay "A Complexity Measure", published in December 1976, Thomas J.Mccabe makes the following opening remark:


There is a critical question facing software engineering today: how to modularize a software system so the resulting modules are both testable and maintainable? That the issues of testability and maintainability are important is borne out by the fact that we often spend half of the development time in testing and can spend most of our dollars maintaining systems.


It amazes me that his remark is still relevant today, almost 50 years after his invention of Cyclomatic Complexity. Writing new code is fun and easy. Making sure you can test it and maintain it for the long run is where things usually start to break.

Mccabe's idea was to quantify complexity in a way that will help software developers decide when their software has passed a certain point in complexity, from which it should be modularized.

Essentially, you'd start coding while eyeing this complexity metric, and when it hits a certain number (say 10), it's time to refactor your code into smaller modules. 

His approach was to measure and control the numbers of paths through a program as a means to reduce complexity. But in my experience (and probably yours), the number of paths in a program is just one single aspect of complexity.

Complexity, as "...the state or quality of being intricate or complicated..." (definition from Oxford Languages) can arise from more than just the number of paths in a program. 

For example:

  1. I noticed that projects which contain modules written in different programming languages (say, Java and Scala, or a mix of Python and Rust) are more complex to extend and maintain than projects written entirely in a single language. 
  2. Usage of 3rd party libraries - while increasing productivity in the short run - sometimes hinder progress at later stages because of the added (hidden) complexity of a black-box in the project. Compatibility is a beast waiting to lift it's ugly head. 
  3. A well tested project (unit-tests, integration-tests, simulators) checks the assumptions about the behavior of its modules - but now developers need to deal with with the project's test code in addition to the project's code to make sense of it. And we know less is more when it comes to the number of LOC...
  4. Using abstract data-types, polymorphism and generalizations reduce the duplication of information in a program, but the number of times I had to peel layers of single line classes to get to the real single line of actual useful code caused me to question some life choices I made.
  5. Naming variables can do so much to increase readability of code, allowing a developer to make sense of code even without too much context. And we know that Context is for Kings
Now, can you make sense of this function?


static int __init setup_elfcorehdr(char *arg)
{
char *end;
if (!arg)
return -EINVAL;
elfcorehdr_addr = memparse(arg, &end);
if (*end == '@') {
elfcorehdr_size = elfcorehdr_addr;
elfcorehdr_addr = memparse(end + 1, &end);
}
return end > arg ? 0 : -EINVAL;
}

My point is that there isn't a single "correct" metric for complexity. Complexity is subjective. "Too complex" means different things to different people. If you're a Linux Kernel expert, the above isn't complex code. For people without any context, who are not familiar with C, the above might look like complete gibberish.

To me, code complexity isn't just about the number of IF ELSE statements or the level of abstraction.

It's about the time it takes the average software engineer to hold a correct mental image of the code in his/her mind, ready to draw upon when needed.

I think this is what Alan Perlis had in mind when he coined the following:

Fools ignore complexity; pragmatists suffer it; experts avoid it; geniuses remove it.

I believe many software engineers have witnessed at least one project which became too complex to be understood even by its own developers. I know I have.

But the worse kind of complexity is Accidental Complexity. Complexity we - as engineers - introduced while solving the problem. It wasn't there to begin with. We added it.

Why? well:

We are too clever for our own good. 

We love to write clever code, but we hate to maintain it. It's the curse of every software engineer out there. We sometimes produce overly-complex solutions to simple problems because we sometimes favor personal interests over the interests of the product. I'm sure you know what I'm talking about. We fall in love with a language feature and decides to use it everywhere. We never used a certain technology and so we designs around it, or we push toward usage of a certain technology because we simply want to learn it.

We need to be aware of the fact that we are the source of Accidental Complexity. It isn't an essential part of the solution - we put it there, either knowingly, or unknowingly. 

Obviously, some complexity can't be avoided no matter what we do - is caused by the problem to be solved. The best way to reduce the gap between accidental and essential complexity is to provide ample time for thinking about the suggested design / implementation and keep asking ourselves: "is there an easier way?"

I'll leave your here with Bjarne Stroustrup's "Make Simple Tasks Simple!" talk from CppCon 2014. 

Enjoy!

Comments