Title Picture from www.freepik.com
Have you ever got into the situation like this:
Your colleagues are fans of clean code and well-structured software, and they follow almost all the rules loyally in your codebase. The methods are short, only do one thing; you don’t repeat yourself anywhere in the code; all the classes are well designed and clearly implemented; the code files are clearly organised into packages; design patterns are properly used; all the best practices are being followed. But when you put yourself into the place to work with the codebase, you are still puzzled — one method calling another, all the objects wired up with each other and each line of code is shouting on your face, but you don’t know what they mean.
Is this kind of code clean? Of course not. From my limited experiences, a lot of people in the IT industry have a strange understanding:
Following rules, principles and best practices = A clean code base
However, in my humble opinion, those rules, principles and best practices had probably help us to fight against our colleagues in code-reviews, but they had never helped me to understand any single lines of code.
What is the problem then?
As developers, we write in a programming language. Programming language is the bridge between computers and human-beings. It is understandable for both sides. It contains information on what to be done, so that computers can do the tasks for us.
However, for human-beings, our concern is NOT “what to be done” at all, but rather why it should be done. But this “why” is seldom being written in the codebase, because computers don’t need this information. However, it is exactly the missing information of “why it should be done like this” has caused all the difficulties while reading the code.
And how can we solve this problem?
Languages are flexible. Writing in a programming language is just like writing in a natural language. With good writing, you can catch the intention of the author, you will follow his thoughts easily. But with bad writing, you can understand each and every word in the sentence, but you can’t understand what the author is talking about.
To write clean code, the goal is not to follow a set of rules, but always ask ourselves one thing — how can I express my intentions with my source code better, so that other human-beings can grasp my ideas easily. This “intention-oriented” thinking can be used at different levels in the code. From naming, to syntax structures, to program structure designs, to overall software structure, or even at a higher level.
Intentions in Naming
As Robert Martin has already stated in his famous book Clean Code, naming should be intention-revealing. Note the wording here, naming shouldn’t be simply “clear”, but “intention-revealing”.
Compare the following code:
function handleCustomEvent(customEventParams) {}
function handleStyleUpdatingEvent(cssStyleAttrs) {}
Both of the namings in the function signatures are trying to clarify something. The difference is the first one is focusing on the underlying implementation, and the second one is focusing on the intention.
We all know we shouldn’t use the short vague namings like result or a. But the additional typing effort should be focusing on intention, its usage, instead of describing itself. It should clarify the “why”, instead of “what”.
In the first function signature, what would it bring to me if I know it is handling the custom event? While in the second function signature, I will have the “aha” moment — it is going to update some sort of style attributes. This will be an important piece of information helping me understand the function implementation more.
Another example:
const deactivatedEntities = entities.filter((e) => e.deactivated);
const entitiesToRemove = entities.filter((e) => e.deactivated);
Both of the constant namings are very descriptive, but the first one is focusing on “what” it is, and the second one is focusing on “why” it is there. What would it bring to me if I know this is an array of deactivated entities? I could know it also from the filter implementation. But if the name is “entities to remove”, then I will immediately know what will come after this line of code. It will help me a lot to understand the implementation.
Implicit Intentions From Syntax Structure
In natural language, we usually have different tones for expressing the same point. For example, if we want to rent a room, we could say: “Is the room still free? I am interested in it. Can we make an appointment so that I can see the room?” Or we could say: “Rent the room to me!” Different tones convey different implicit information. And according to the different implicit information being conveyed, we usually get different understandings.
It is also true in programming languages. Even if the functionality is the same, if we write code in different ways, different information will be carried with the code.
Consider about different ways to implement a loop:
// Variant 1
for (const elem of arr) { ... }
// Variant 2
for (let i = 0; i < arr.length; i++) {
const elem = arr[i];
...
}
// Variant 3
let i = 0;
while (i < arr.length) {
const elem = arr[i];
...
i++;
}
// Variant 4
function processElemRecursively(arr) {
if (arr.length === 0) {
return;
}
const elem = arr.shift();
...
processElemRecursively(arr);
}
processElemRecursively(arr);
// Variant 5
function processFirstElemInArr(arr) {
if (arr.length === 0) {
return;
}
const elem = arr.shift();
...
processElemInArr(arr);
}
function processRestElemInArr(arr) {
...
processFirstElem(arr);
}
processRestElemInArr(arr);
From the point of view of the final result, all these 5 variants are the same. But:
- In variant 1, the implicit information from
for (of)(foreach in JavaScript) is that I just want to do something with each element in the array, and I don’t care about anything else. - In variant 2, the implicit message carried by traditional C style
for (;;)is that I want to go through each element in the array, but I do care about their order and/or position in the array too. - In variant 3, the implicit message from
whileis that I don’t actually care about whether I have gone through all the elements in the array, but I just want to do things repetitively. - In variant 4, the implicit message from recursive call is that I want to do the same thing for each element in the array (or a collection, e.g. tree, which can make iteration using
whilevery difficult), but I might want to control the way how I fetch the next element. - In variant 5, the implicit message from the ping-ping recursive call is that I want to do the same thing for each element in the array (or a collection, e.g. tree), but I might want to do something extra to control the way how I fetch the next element based on the result of processing the element.
And since in this example, without any more context, the code makes an impression that I would like to process each element in the array. You will probably find the first 2 variants are more natural to read, and this is exactly my point here. From different ways of writing the same piece of code, we will get a different vibe, and this vibe is usually revealing our intentions. By choosing the vibe carefully, we can express our intention much better.
The similar differences also come from the usage of if-elses vs switch-cases, or inline lambda vs named functions. If we ask ourselves more often, why using one variant instead of the other one, what is the intention behind a certain variant, slowly, we will also know which variant to choose to express our intentions better.
Intentions Carried by Design Patterns
Let’s go one level up from the syntax structure, then it is the scope where several components in the program are working with each other. At this level, I’d like to talk about design patterns.
In real life, if you see a person carrying a plunger, you know that person is going to plumb the drain hole. If you see a person carrying a drill, you know that person is going to drill a hole. If you see a person carrying a pile of wires, screws, metal sheets, pipes, you would wonder, what is he going to do?
When I was in university, I didn’t have much experience developing software in a big scope. But still I was being forced to learn design patterns. Years later, I felt that it was just like showing you how to turn a screw with power tools but not show you a screw hole. Or showing you how to cut the wood with a saw but don’t show you the crate made of wood plates. The missing part is the intentions carried by design patterns.
Each and every design pattern has been invented to solve a problem. But while reading the code, by simply looking at the name of the design pattern, we can imagine what kind of problem in front of us to be solved.
Just imagine the difference between these two variants:
- There is a global enum typed variable, a set of functions changing its value. A lot of if-elses are based on the value of this variable.
- There is a global object called
ApplicationModeStateMachine.
Now these two variants are implementing the same functionality, which one makes the situation more clear? I guess it is definitely variant 2. Because the intention, or the problem that the developer is trying to solve has been described in a single phrase — “state machine”.
That is exactly the power of design patterns in the development. It is not only solving the problem, but also telling the reader of the code which problem has been solved. It is just like if someone is carrying a plunger, you won’t think he is going to see a piece of wood.
Although the variation of a loose bunch of functions and a global enum variable is implicitly implementing the finite state machine too, without mentioning the name of the design pattern, it would take a long time until the code reader notices what it actually is.
Besides, it also helps people to create a sense of separation of the code, so that people can relate code with the intention even more clearly. With the clear naming mentioning “state machine”, we will clearly know that there will be a bunch of state handlers, and there will be state transitions, and there will be state transition hooks and callbacks. We know what to expect, which will also make it easier for us to find the code related to this topic. And most importantly, it will also help us to know, which part of the code does NOT belong to this topic. In this way, we made a clear boundary in the code, the relationship between the code and the intention will also get clearer.
Intention of Sub-Systems and Layers
The worst code that I have ever written is usually coming from a certain type of flow-mode, where people feel very productive and they write several thousands lines of code within one single night without thinking much about the overall structure. And the result usually has a unix mode 333 — for every one, it can be only written or executed, but not read.
The structure must follow a very clear intention, and the contributors of the structure must follow the intention too.
A typical example is actually the example I have mentioned at the beginning of this article. But why does the project end like that? It is because people only follow the rules and principles only knowing a “local why”, instead of a “global why”.
Why should I extract this part of the code? Because it is duplicated with another piece of code. This is what I call the “local why”. A lot of people seem to stuck at this point, they focus on the “local why” too much, so that:
- They extract thousands utility methods out of a class just for reuse it twice
- They create thousands of abstract layers to follow the DRY principle
But eventually, edge cases come up. Their abstraction is not proper anymore. Other developers, or even themselves are so frustrated to touch thousands of levels of abstract layers, and they write thousands of if-elses in different abstract layers to make things work — the code gets rotten.
I have seen too many cases like this.
Before we do any new level of abstraction, or before we create any new sub-system or sub-component for the system, firstly ask ourselves a question. Why does the team need another layer of abstraction, or why does the team need another sub-system or sub-component?
Note that I mentioned “why the team …”, instead of “why the software …”, because it is us human-beings who need the intention of the new sub-system or layer, not the machines. As I have mentioned earlier, machines don’t need intentions, they need instructions.
In my opinion, a good software structure should be divided into a small number of sub-systems or layers, where the intentions of each of the sub-systems or layers are clear. Once the intention is clear, the interaction between the sub-systems and layers should also follow intuitively. Each and everyone in the team should think about how to keep the simplicity, instead of creating more unnecessary abstract layers.
The more abstract layers we have, the more vague are the intentions of each abstract layer. The more vague the intentions of each abstract layer are, the harder it is to read the code. By pushing hard on this way far enough, one will notice that the whole project has become a well made spaghetti code — the main purpose of the sub-systems, components and layers of the software is hidden in thousands of details, one can’t figure out what is connected to what anymore, making it actually no different than the bad written spaghetti code.
A good design must be simple and intention-revealing. Just like no matter how complex a story is, it is always possible to write a good summary to cover the main progression of the story. Just try to tell a stranger with minimum technical background about the whole project within 15 minutes. If you fail to do that, probably the new employee in the team will also struggle to understand the code.
Describing Intention as a General Mindset
Development is far beyond writing code, but it is all about communication and knowledge sharing, either with other colleagues, or with computers. However, I noticed that, with some of the developers, I was always having a smooth communication. I can grasp their ideas very quickly, and I can even predict what is coming up next. But with other developers, I have to focus on their words very carefully, before I fully understand what they mean.
This is because, some developers always put their goal at the beginning, e.g. “I wanted to do …, that’s why I have written …” This style of communication is far more effective than “I have written … So that I could do …”. This also applies to the process while reading the documents.
Sometimes it is hard for technical people to focus on the intention while talking or writing. It is because as technicians, we think about solutions, not the questions. But for communication, no matter using a natural language to communicate with human-beings, or using a programming language to communicate with computers AND human-beings, state the intention becomes of vital importance. With a question clearly stated, people will automatically start thinking about the solutions, even before we start to elaborate further. This will create engagement for both sides of the communicational channel, and this is exactly what we need.


