There is a book called Human Error, by James Reason, that has been very influential in industries like nuclear power plants and aviation. It is full of research information and citations, which make it a great read for people who want to know all the details. For those who just want an overview, a good recommendation is The Design of Everyday Things by Don Norman. The following paragraphs are the result of applying some of those ideas to software design.
Having an accurate mental model of a system is a great tool for preventing errors: if you know how a system works—if you are familiar with its internal design—, you will be less likely to use it in the wrong way. But having an accurate mental model is also important for fixing errors within the system: when something goes wrong, you will have an idea of what part could be at fault. Closely related is the concept of situational awareness: it refers to the capture and processing of external conditions that may affect the system. Our mental model must take those variables into consideration.
As an example of the risks carried by a defective mental model, here is an exchange between a pilot in his small plane and the ground controller. The pilot is alone and in distress, probably because of bad weather conditions.
Pilot:
Mayday Mayday Mayday! N9815L, I’m in trouble! Mayday Mayday Mayday!Controller:
N9815L, Fort Dodge, go ahead.Pilot:
I have no idea where I’m going, I’m going to crash! Mayday Mayday Mayday!Controller:
9815L, say last known position.Pilot:
I have no idea, I have no idea! Mayday Mayday Mayday! I’m gonna crash!Controller:
9815L, say altitude.Pilot:
I’m rolling! I’m rolling! I’m rolling! I’m rolling! Oh my god! Help! Help!Controller:
Calling Fort Dodge, release the stick, go forward on the stick then slowly back again.Pilot:
I’m level! I’m straight and level! I don’t know where I am!
The conversation continues and the pilot lands safely, you can listen to it here. Having a good mental model of the situation allowed the ground controller to provide life-saving help with just a few directives.
That kind of intuition comes from experience, but there are two shortcuts for attaining it: good habits and simplicity. By having a simpler system, we have better chances of forming a good mental model. By having good habits, we avoid a good deal of error scenarios and we have better chances to recall the proper set of rules for coping with a crisis. A simpler system is easier to understand, as complexity is the main barrier for creating an accurate mental model.
In programming, there are several tools for measuring complexity. The term Software Complexity refers to the relationship between code and programmer: it has to do with how difficult it is for a person to understand what the code does. It is unrelated to the computational complexity, and it has nothing to do with how well designed the code is. A novice programmer can create a program that is simple, elegant and wrong. An advanced programmer can create a program that is correct, but complex and hard to understand. For the purpose of creating an accurate mental model, even the program’s correctness is of secondary importance: code that is understandable can be fixed. Of course, we should not allow incorrect code, and moving forward we will assume we are discussing code that works, code that is clear and readable, and we will focus on its software complexity.
There is no perfect metric for software complexity. That said, some metrics have good empirical correlation with what we experience when trying to understand some program. Two metrics worth mentioning are Cyclomatic Complexity (McCabe, 1976) and Halstead Code Volume (Halstead, 1977). There are tools in almost every programming language for measuring those values. A paper about correlations between internal software metrics and software dependability (van der Meulen & Revilla, 2007) demonstrates that software complexity is proportional to the lines of code of a program. The correlation is linear, positive and almost perfect. Counting the lines of code provides a great estimation of how difficult it is to understand some code. Again, there is no perfect metric, and the experience of the programmer plays a key role. All things being equal, the less code you have to read the easier it will be for you to understand what it does.
We write code in order to solve problems, but the code we write is itself a new problem: it will use memory, CPU cycles, and its complexity will get in the way if somebody —or your future self— needs to understand it. By solving problems with code, we are trading an original problem for an artificial one. This new problem should be easier to deal with; if that is not the case, we are not making any progress.
How complex can a program be? How complex should it be? There is no definitive answer, but we can test the complexity boundaries. For any correct program —a program that yields the right result—, we can create a more complex version that is still correct. In fact, there is no limit to the amount of complexity we can add to a program, and it will be correct as long as it keeps producing the right result.
Given two programs that yield the same result, the simpler one is to be preferred because it helps us build a good mental model. Of secondary importance is the frugal use of resources: the program with better performance and less memory usage is to be preferred. It means that in many situations, we should be ready to sacrifice some performance if the result is a simpler program. Luckily, simpler programs tend to be more efficient in their use of resources.
Why is our industry producing tools with such high level of complexity? Could it be the case that we are overconfident in our mental models in the face of complex programs? The pilot in that story, I suppose, was confident his mental model was good enough. In the same way, every other month we read the post mortem report by a company that had a half-day downtime, all the while thinking, I assume, that their mental model was adequate. In retrospect, when analyzing accidents, we can verify that mental models of complex systems like distributed programs, airplanes and power plants, are usually wrong.
A while ago, I tried my best to explain the benefits of simple tools
in a presentation about minimalism in programming. While I could not
tell for sure, my impression was that people already aligned with those
ideals where the ones who enjoyed the presentation, and I did not
manage to convince anyone else. The next speaker in line could not
restrain himself and said: I also love minimalism, and that is
why I love Rails!
. Rails is a project that makes a lot of
money for a lot of people. It has fans everywhere, and it is handy for
starting a project if you already know how to use it. But I would never
call it minimalist. Why did my presentation provoke such reaction?
Could it be that our lack of scientific rigor promotes that kind of
relativistic approach? Maybe there is some kind of emotional attachment
that blocks any criticism. In any case, I wish I knew better.
In 2011, Rich Hickey delivered a great presentation called Simple Made Easy where he explained the difference between simple and complex, but most importantly he explored why we usually conflate the concepts simple and easy: something can be objectively simpler, but only subjectively easier. He did a reprise at RailsConf 2012, also worth watching. Understanding the distinction between simple and easy is key for suppressing any knee-jerk reaction when someone denounces a tool as too complex.
What happens if we do not strive for simplicity? An article by Leslie Lamport discuses the future of computing, and wonders if it will be like logic or like biology. He uses logic to label programs that are simple and can be analyzed and proved correct. On the other hand, biology refers to systems so complex that we no longer understand how they work, and we can only focus on how they behave. Dealing with something we do not fully understand can lead to irrational behavior, he argues. A person that does not understand how the human body works may use homeopathy. But that same person would never use something homeopathy-like with a simpler system: nobody uses homeopathy to fix a broken car. With software, something similar happens: some people may select a tool because it gets constantly updated, a defect often regarded as a virtue. Others may reject a tool because it did not gather enough stars on GitHub, while the rational behavior would be to read the code, understand what it does, and reject it if it does not work for their use case.
We are good at making mistakes, and extremely bad at being precise. If we give up trying to understand, we are setting up the perfect trap for ourselves.
⊙