Local Code Complexity

Viktor Zatorskyi
3 min readMay 28, 2020

I got into development because it was fun. Only later, I had realized that more often than not, software development is a headache. Modern software practices focus on delivering functionality at all costs, sometimes with compromises to computational and cognitive complexity. The later contributes heavily to the level of depression and anxiety in life.

The older codebase becomes the more time we spend reading the code before adding something or fixing the issue. In extreme scenarios, days of debugging could lead to one line change. The easier the code, the less mental burden we have during our work. But how does the easy to read code look like?

I’d like to begin with separating code complexity in two — Global and Local complexities. Local Code Complexity is computed on a function level. Global one is calculated on an application level. Overall system design and the structure of a call-stack are related to the Global level.

Local complexity was computed for ages with Cyclomatic complexity. The control flow path is adding +1 to complexity. It doesn’t represent a correct mental load.

void Example1(){
switch(num){
case 1: ...
case 2: ...
case 3: ...
case 4: ...
}
}
------------------------------------------------------
void Example2(){
if(num = 1){
if( num == 2) {
...
} else {
...
}
} else if( num == 4 ){
...
}
}

Both of the examples have the same cyclomatic complexity but different mental loads.

Cognitive Complexity metric improved this situation a bit by punishing nested logic. Early returns and shorthand notation are preferred.

Another metric that people often use is a function length, and it’s often considered that shorter methods are preferred and easy to understand. One of the solutions is to reduce Cyclomatic and Cognitive complexities by factoring code into another function. But I can’t agree that it makes the system easier to understand overall. We are just pushing complexity from Local level to Global level. What eventually may happen is that we end up with fractured codebase and very deep call-stack.

We have to work on both levels to create balanced and easy to support system. Here is how I prefer to manage Local complexity.

Early returns: I totally love them. They make code structured linearly. I consider them as a reduce operation. Early returns filter everything that doesn’t suit, and I have to work only with what’s left. They don’t introduce much mental load, to me at least.

Shorthand notation: Ternary operators in assignments, Null condition operator in C#, Null coalescing operator in C#, and so on. They still have some mental load, but I find it much less than with regular branching.

Linear execution: I try to avoid nesting. Each branching operation adds some state we need to remember in our Mental Stack, and we, humans, aren’t very good at memorizing state. I prefer to map/reduce data in several passes when performance is not critical. Or use a data-oriented design approach where all data processing happens in the small, highly focused systems on streams of data.

Naming: By introducing names, we present our mental model of the problem. Code readers have to understand not only the problem itself but also the mental model of our solution: the purer our model and the lesser abstraction on top of the problem we have, the better.

Comments: Don’t explain what happens, explain why it happens. Give intuitions to why this particular data structure was chosen. Provide reasons why a specific equation was used in the calculation, etc.

One of the best ways to get better at reducing your Local Complexity is solving LeetCode problems. It’s crucial to see the solutions from other people. They don’t always come in most readable ways, and that what I urge not to copy, but the sheer minimalism of the code could inspire to write functions cleaner.

Next time I’ll cover my vision of Global Code Complexity.

--

--