When Jia Dao first went to the capital for the imperial examination, one day on his donkey he composed a line: “Birds roost by the pond-side tree, a monk knocks at the moonlit door.” At first he wanted to use “push,” then he wanted to use “knock,” and could not decide after much refinement. So he kept chanting it on his donkey, occasionally reaching out to make the gesture of pushing and knocking.
—— Song · Hu Zi, Tiaoxi Yuyin Conghua, Vol. 19, quoting Liu Gong Jiahua
Naming is the most crucial thing in coding; to a program, it is like a face to a young girl. Good naming can clearly convey the intent of the code, and even possess a sense of rhythmic beauty. Slack and casual naming, on the other hand, makes one feel as if fallen into a fog, unbearable to read, consuming the maintainer’s energy again and again. Moreover, a chaotic naming system can effortlessly hide bugs and cause harm far and wide.
Therefore, when we write code, it is necessary to spend a little time scrutinizing key names — for the convenience of others and ourselves. The longer the lifespan of a project, the more obvious the benefits. So how should we scrutinize? Let me share some of my thoughts, drawing on my experience in writing code, reading code, and reviewing code.
I’ve been writing more Go recently, so the examples use Go, but they are all pseudocode; some examples do not strictly follow the syntax. Moreover, most examples are made up on the spot, so they may not be particularly apt.
Author: 木鸟杂记 https://www.qtmuniao.com/2021/12/12/how-to-write-code-scrutinize-names Please indicate the source when reposting
Honesty
When it comes to naming, there are actually many principles. After much thought, I feel the most important idea to emphasize is — Honesty (laughs). When we write or modify code, we must never sell dog meat as mutton, becoming intentional or unintentional fraudsters. In other words, names must truthfully and completely reflect intent. Otherwise, maintainers can easily be misled by a name, forming preconceptions, overlooking some details, and even overlooking some bugs.
There are several most common situations.
First, the function implements logic A, but is casually given a name B. This happens either because the naming was too casual, thinking it doesn’t matter, or because the function logic is too complex and a suitable name cannot be found, so a random one was assigned. The former is an attitude problem, which we won’t dwell on. The latter generally requires splitting the logic until the name can clearly reflect its logic.
Second, the function has side effects, but the name does not reflect them.
1 | func (c *Company) check(p *Person) { |
In the code above, since the name says it only checks, it must not secretly set. If you also want to set, you can rename it to checkAndSet().
Another typical example: it looks like a pure function, but secretly changes global state.
Third, the code was changed, but the name was not changed accordingly. For example, when we refactor a function, we stuff in some new logic but keep its name unchanged; or we completely changed the code logic but did not change the name; of course, the most common case is that the code changed but the comments were forgotten.
1 | // pseudocode |
Fourth, using conventional names to refer to different meanings. For example, in Go, when we talk about context, we usually think of the Context in the official library used to control lifecycle. So when naming, don’t casually occupy this name. For instance, in some cases we need a class that saves the task execution context; be sure to add a prefix, such as JobContext. If you directly use Context as the class name, even if one can understand its meaning after careful reading, it will still feel very awkward.
Explicitness
When we implement a certain feature, we repeatedly think about many details; these details are the context of our thoughts at that time. Immersed in this context, we easily write code that feels incredibly natural at the time but incredibly confusing later. Because as time passes, the context in your brain disappears.
For example, when checking a certain condition, using an obscure, indirect, or counter-intuitive statement. In such cases, it can be replaced with a more direct signal, or by adding a variable and using the variable name to explain.
1 | func (p *Project) Get(req *GetRequest) (*GetResponse, error){ |
In the example above, the original intent was: when req.names is a non-empty array, only return the info for the specified students; when req.names == nil, it indicates all students in the project group, and the teacher’s info needs to be returned additionally. But the latter information is completely implicit and counter-intuitive.
It can be made explicit through some means, for example bool isAll = req.names == nil. Similarly, for longer anonymous functions, it is best to assign them to a variable and use the variable name to explain the function.
Therefore, it is best to constantly put yourself in others’ shoes, continuously eliminating implicit context dependencies, in order to write intuitive code that does not require too many comments. In addition, having several people who lack this implementation-details context (but preferably understand the design) review the code is also a good way to eliminate implicit dependencies.
Conciseness
Some names are simply long and convoluted. One must know that names exceeding three words do not make the semantics any clearer, and instead increase the burden of understanding. This usually happens because there wasn’t careful scrutiny to leverage program structure to eliminate redundant information. Let me give a few examples.
First, eliminate redundancy through hierarchical information. Such as package names (or namespaces, depending on the language) and class names.
1 | package student |
Since the package name already indicates student, the function name can omit the Student part, i.e., change it to func New()(*Student, error). When used as student.New(), the type of the object being created is clearly visible.
From the perspective of data structures, organizing concepts in a tree allows the naming of individual tree nodes to become relatively simple, while using the tree path to express sufficiently rich meaning.
Second, use parameter names to eliminate redundancy.
1 | func handleStudent(student *Student) |
The parameter name already indicates the object being processed by the function, so the function name does not need to repeat it: func handle(student *Student).
Third, the smaller the scope, the shorter the name can be. The most common examples are iteration variables and local variables in small functions. Because the smaller the scope, the smaller the possibility of conflict.
1 | // case 1: iteration |
But conciseness has its limits, bounded by not introducing ambiguity. Still taking the Student example above:
1 | package student |
Here, the New function is a bit too short, because when called as student.New() it is easy to be misunderstood as returning a Student type. Therefore, it is better to change it to student.NewManager().
Systematicity
Finally, but also most importantly, naming needs to be systematic. And only with sufficient understanding of the problem being solved can one make sufficiently apt abstractions, write sufficiently concise code, and give names that are short, symmetric, and consistent. Still borrowing from data structures to summarize the organizational principles of a naming system: from a macro perspective, code is organized in a layered tree; from a micro perspective, layers are organized like a bipartite graph.
Let me give a few examples from several angles.
First, consistency, compatibility. In one’s own code design, this manifests as consistency in style across multiple components; when modifying others’ code, it manifests as compatibility by continuing their style.
1 | // student.go |
The example above is a negative case: the same meaning is expressed using different names. What is worse is that similar places and similar names refer to different meanings. This brings a huge mental burden to maintainers.
Second, atomicity, orthogonality. Individual functions should be as short as possible, so that function names can fully reflect the code’s meaning; basic functions should be as orthogonal as possible, so as to remove redundancy and express powerful capability through combination.
A common example is CRUD around a certain entity in a Web service, such as a StudentManager for Student. The upper layers can then use these basic create, read, update, and delete operations to accomplish more detailed business logic.
1 | type StudentManager struct {} |
Third, systematicity. Use some means to decompose the system into several very natural modules. This naturalness essentially leverages the shared context between you and the reader.
For example, when needing to adapt multiple storage backends, the abstraction regarding Storage.
1 | type Storage interface { |
For example, when needing to manage task lifecycles, the abstraction regarding Task.
1 | type Task interface { |
The usual practice is to draw on some common abstractions in data structures, operating systems, databases, and networking — such as message queues, file systems, processes and threads, transport protocols, etc. — and make appropriate variations to serve our needs. Most programmers share these basic concepts, so they can quickly grasp the structure and logic of your code.
Another commonly used approach is to borrow concepts familiar to everyone in daily life, and decompose the system around their characteristics, space-time properties, and so on. For example, at a company I previously worked for, concepts such as Eagle, Eagle Eye (monitoring), and Eagle Claw (crawling) were used to decompose a monitoring system.
Summary
When ancient people wrote articles, they emphasized repeated scrutiny to produce good works. Code naming also requires careful tempering in order to continuously extend its lifespan and avoid rapid rot. If one only pursues writing code quickly, using whatever comes to mind as the name, the code will inevitably be destined to be refactored or even abandoned after running once.
Limited by my knowledge, there must be many omissions. Regarding code naming, what other insights or complaints do you have? Welcome to leave a comment and discuss.
