Cognitive load in programming: why the brain is the bottleneck
Controlling complexity is the essence of computer programming.
— Brian Kernighan
In programming we always abhor of complexity. This hatred for complexity has empirical roots: over decades programmers have consistently experienced how complex programs are error prone and cumbersome to change. But we never try to explain why complex programs are harder to modify. Cognitive psychologists, on the other hand, have spent the last few decades studying complexity. They’ve developed a theoretical framework to explain how complexity affects reasoning and errors in thinking. Their conclusions are clear: the brain has a very limited capacity for reasoning. We need to acknowledge these ideas and accept that the brain is the main bottleneck in software development so we can come up with more efficient software design principles.
Why progress is faster in greenfield projects?
A clear example of how the brain is the bottleneck is legacy code. When you work on a legacy project your progress is slower than when you work on a greenfield project. Why is that? Because to make changes in a greenfield project you need little context, while to make changes in legacy code you must first understand the intricacies of the existing code. In a legacy project you need to load the existing code before making a change. This is slow and expensive.
For example, in a web application you may have to figure out if changing a class in the login code would also affect the login process in the mobile app. Understanding how login works in the web and mobile apps will take some time and mental effort. None of that is required in a greenfield project, where you may start implementing a login page without having to understand the existing code.
Loading context into your brain is very expensive.
Working memory and cognitive load
Psychologists have been studying for years the limitations of your brain and the results are humbling. Your brain can only make logical reasoning about concepts available in a short term memory that cognitive psychologists call the working memory (WM). The working memory is the part of our brain that consciously processes information. It’s very limited: it can only hold up to 4 or 5 elements and it lasts only for about 10 seconds.
Working memory is also very sensitive to overload. Errors in common tasks, like remembering a sequence of elements or simple mental reasoning, skyrocket when the working memory is over capacity.
Fortunately, the brain has other systems to store information. Even if the brain has a very limited working memory, it also has a much bigger long term memory (LTM). The LTM stores information in the form of schemas, which are patterns that help organise and interpret information.
The working memory and the LTM interact with each other. The brain can spend some of its processing power creating schemas and store them in the LTM. The working memory can also load schemas from the LTM to use them in logical reasoning.
The load imposed in the working memory at any given moment is called the cognitive load. There are three types of cognitive load:
Intrinsic cognitive load: this is related to the intrinsic complexity of the task. For example, adding two numbers is easier than solving a differential equation. There is no way around intrinsic cognitive load.
Extraneous cognitive load: this is generated by the manner in which the information is presented. For instance, it is easier to explain what a icosahedron is showing a 20 faces dice that with a verbal description. Extraneous cognitive load is unnecessary and should be avoided.
Germane cognitive load: this is the brain capacity that we employ to build and process new schemas.
Learning materials are designed to minimise extraneous load and maximise germane load.
Good software design minimises cognitive load
Software design principles are mostly attempts to minimise cognitive load. Good design simplifies making changes to existing code by limiting the amount of context you have to hold in your working memory. If in order to change a class you need to understand 12 other classes that use it, you will find very difficult to hold all that information in the working memory and make the change. It is much more likely that you’ll make a mistake.
Good design always tries to avoid cognitive overload. For instance we can use encapsulation; we provide abstractions and clear interfaces to hide the underlying complexity. To make a change in a well encapsulated code you only need to understand the exposed API, you don’t have to understand how the client code works or the details of the implementation. That makes the easier for your brain to hold the necessary information in the working memory and reason about it without errors.
Encapsulation is never perfect. Abstractions leak, and and often you have to worry about how an API is implemented, but it’s the best we can do to keep cognitive load controlled.
Sometimes we also hear the opposite advice. For instance, we can make our tests run faster if we are willing to pay a price in added complexity. I like fast tests like anyone else but I’m always very wary of trading speed for complexity. Computer cycles are cheap and easily scalable. Computer power can scale horizontally (buying more computers) or vertically (buying more powerful computers). But your brain capacity is mostly fixed and painfully limited. In programming, the brain is often the main bottleneck.