How to Improve Code Quality?
Good programmers never make a living off plaid shirts or good looks — just as your Cristiano Ronaldo, who could easily coast on his face, insists instead on charming you with a body and a skill set honed ever finer. For a great striker, goals — and a body capable of unleashing the ability to score them — are what matter.
For a dependable programmer, code quality — and a mind that can see through to the way high-quality software is written — is precious beyond measure.
This essay discusses how to improve code quality from the angles of product, interface, metrics, logs, code clarity, and code complexity.
Products and Interfaces
A good product manager isn’t necessarily a good programmer, but a good programmer is certainly a good product manager.
What is a product manager’s job? To present complex logic to users in a clear, easy-to-use form (an interface).
A programmer’s product is code, and the users of code are other programmers — so high-quality code is code that other programmers find easy to understand and easy to use. Note that at this level, “easy to understand” means easy to understand in structure, principle, and interface — not in the fine details of the code.
At the product level, details must absolutely be hidden. When a user opens a browser and visits arcblock.io, they needn’t care how DNS works, how the PKI system operates, what the HTTP / TLS / TCP / IP protocols are, how a packet is delivered from user space to kernel space, how it’s then DMA’d to the network port and sent out — and that’s not even the end of it. Next onto the stage come the switch handling the L2 protocol, the firewall protecting you, the router playing postman, and NAT — which in concept is networking tech but squandered its entire youth serving security…
If a product manager’s product showed users that kind of blah-blah detail, then the poor sod would have his temp residence permit torn up, get slapped silly from dusk till dawn, and be shipped off on the morning green train to Qinghe to shovel sand; if a programmer’s main() were that verbose — whether or not anyone could stand it — then he’s surely destined to never find a friend of the same sex, let alone the opposite.
So before a programmer writes code, they should first think: if this were a speech, how would I begin? Can I explain in three to five minutes what this code does? Is there an example from daily life — or one that makes fellow programmers smile in instant recognition — that I can use as an analogy?
In more than 90% of cases, a programmer is writing a parser. In other words, the vast majority of code we write takes a series of inputs, runs them through some number of transformations, and produces a series of outputs.

Some concrete examples.
A front-end engineer parses the user’s URL request into a series of components on the browser’s DOM, parses the user’s actions into some internal event {event_type, event_data}, and further parses event_type into some event_handler — then that handler keeps parsing event_data until it’s turned into new DOM, or into some request to some API on the backend.
For an API, it parses a request and produces a response. The request might be parsed into a SQL statement handed to a database; it might also be parsed into a request satisfying another service’s interface (say, gRPC) and handed to another service. Thus it goes round and round, until the API has gathered all seven Dragon Balls to summon the dragon all the data from every service, then parses it into a compliant response and hands it back to the client.
So a programmer should regard their code product the way Cook Ding regarded the ox — “he no longer saw the whole ox,” “he met it with his spirit rather than looking with his eyes,” “he inserted what had no thickness into what had gaps” — everywhere you look are parsers, big parsers hanging with small parsers, which hang with still smaller parsers.
Every layer, indeed every parser, is a pipeline — generally organized from interfaces like validator, serializer, and transformer, aided by assorted builders, decorators, factories, and commanders, plus the tools, utilities, and helpers that spring up to serve them.
Organized layer upon layer like this, the coarse places are coarse and the fine places are fine; speak human to humans, speak ape to apes; the code can be cute or commanding as the moment demands.
Next comes the most important yet most head-scratching task: naming your modules, big and small.
A name pours in feeling — like the match the little girl strikes on a cold night, like the wand Gandalf raises outside Helm’s Deep after a night of fierce battle. It gives light, warmth, hope, and, when you read it, the electric jolt of “I get you.”
There’s a scene in The Shawshank Redemption where, at lunch, Andy asks everyone the name of that poor fat guy who was beaten to death the night before. Everyone’s baffled: why the hell should I care about the name of a dead fat guy? That scene hurts to watch — like Hua’an, before he became Hua’an, having only a designation as tiny as an ant.
If you want your code not to be a dead fat guy people ignore, but something people are willing to talk about, then give it a name that’s easy to understand — even unforgettable.
A bad name leaves people puzzled, indifferent — even like a malicious ghost cut off from the world of the living: one glance and everyone wants to flee. A good name — well, just to pick one — Nie Xiaoqian: equally a lone ghost severed from the mortal world, yet you and I can’t stop thinking of her.
At Juniper, the two servers I can never forget are gretel and hansel, taken from the Brothers Grimm. At Tuke Circle, the projects I kept thinking about were atlantis and, atop it, viking (code name) — no mystery there: to seek atlantis, you need a viking expedition. At tubi, cms service was a lousy name; merlin was a return to form, though as a build service its magic isn’t all that strong and it misfires now and then. And at arcblock, the AADL I discussed in my last post was formally named AODL — no big deal, you probably won’t remember it either. But she did get a public name: goldorin — Elvish, the language Tolkien invented for the elves of Middle-earth.
In “A Monk Knocks on the Moonlit Gate,” I mentioned how pitcher / catcher in the obscure IKE code made the protocol’s negotiation read like watching a baseball game. Good names and good interfaces almost always show up in pairs; they make the programmer’s product — code — vivid, reading like a spring breeze on your face, like fine wine, like beholding a beauty.
Metrics and Logs
A good product is refined through improvement, like the phoenix, tempered again and again in fire until it attains nirvana. And to improve, you can’t do without measurement (measure) — it’s the most important link between build and learn.

The second law of thermodynamics is the most annoying and most inescapable of laws. It directly causes all these vexing things: “skip exercise and belly flab inevitably grows,” “don’t tidy the house and it gets messier and messier,” “don’t keep improving your code and its quality keeps sinking.”
And the patriarch of this blasted law, Lord Kelvin, said:

Yes — measurement is important, very important. If build and improve are two branches, measurement is the web a spider strings between them. The denser the web, the more paths between the two branches, and the easier to go from one end to the other, round and round.
Measurement comes chiefly through metrics and logs. Metrics are like an ECG or a CT scan, laying the body’s condition bare. So metrics are for understanding the present state and pointing out direction; logs are a fine-grained diary — they contain everything and no highlights at all, so once the current state and problem direction are pinned down, they’re used for attribution. For instance, the CT report says: compared with last week, this week your liver isn’t doing so well — take good care of it. So why is the liver unhappy? Pull up the week’s logs and there it is: every night at the bar, reveling in the World Cup. So the improvement plan emerges: once the World Cup is over, and you stop both drinking and staying up late, the ailment will clear right up — no problem.
Most of the time, metrics and logs are read by yourself and other programmers, so from the earlier perspective, they too are products and obey every rule of products and interfaces.
First, metrics.
When defining metrics, you first need to understand what you’re trying to improve — this is the so-called begin with the end in mind. Runtime efficiency of the code? Then where exactly is it inefficient? How do you define efficiency, how do you compute it (latency? throughput? something else)? Fault tolerance of the code? Then what kinds of errors should be collected, and how classified? Where are the potential strongholds of error?
Once you know what to improve, you need a picture in your head — not “Dwelling in the Fuchun Mountains” — but a pre-visualization of yourself or others using these metrics, the way Zhizunbao showed the bandits the picture of his timeless, unprecedented love with Bai Jingjing.
For instance, you want to improve efficiency, and you’ve determined it’s about lowering latency, so you plan to collect the service’s response time — then should response time be shown as a line chart or a bar chart? Once you know latency has suddenly risen, what’s next? How do you know what to look at next? Should it correlate with other metrics / events? Which ones, and how? Think about after an incident: as the sole programmer available to take the blame, with a crowd of product and ops folks staring at your screen behind you, all wearing long faces more solemn than a funeral, as if you’d just cost the company hundreds of thousands per second. With nervous sweat soaking through your plaid shirt, what can you look at, what should you look at?
Defining things layer by layer like this — what problem to solve, what metrics to collect, how to correlate and use metrics — lets us guarantee two things: 1. When something bad happens, I’m the first to know. E.g., the 95th-percentile response time of a public API jumped 30% in the past 5 minutes. 2. I can pin down the rough scope of the problem. E.g., other metrics show it’s because diagon alley’s latency suddenly rose, and further, diagon alley’s disk write IOPS climbed sharply. So now I look into why diagon alley’s disk writes are abnormal.
Next, logs.
Logs are weird important data that you needn’t fuss over when nothing’s wrong, but which must let you locate the specific spot easily the moment something goes wrong. So logs should be plentiful and specific, broad and complete like a comprehensive dictionary — for instance, when metrics tell us the problem lies in our not knowing how many ways there are to write the character 茴 in “fennel bean,” logs should help us quickly flip to the useful passage and find the four ways to write 茴.
Logs serve double duty — read by humans and analyzed by machines — so it’s best to fix the format for the sake of machine analysis; but don’t use machine-readable, JSON-like formats, because without a good parser, reading them as a human is like looking at Korean-plastic-surgery soccer babes, or sentences with every adjective sucked dry: they all look identical, and you have to take off your glasses and squint through a magnifying glass to spot the differences.
With sensible metrics and logs, measurement becomes within easy reach. That unlocks our ability to keep iterating and improving. With code at the same starting point and programmers of equal caliber, one iterating once a week and the other once a day — after several cycles, their accumulated quality differs qualitatively.
Code Clarity and Code Complexity
If all the above is done well, then however bad the code quality gets, it has a floor. That floor can be raised by strictly using a linter and continuously deepening your mastery of the language in use. It’s like a person who thinks independently and thinks often — their writing is worth reading. Maybe in wording, in rhetorical flourish, in structure, they’re still green, but that’s the floor, and an easily raised floor.
In the Elixir linter, I set ABC complexity size to 70 and cyclomatic complexity to 15. ABC complexity is the square root of the sum of squares of assignments (A), branches (B), and conditionals (C) in the code; it represents how verbose a chunk of code is. Cyclomatic complexity — loop complexity — is the number of linearly independent paths measured through the source code; it represents how hard a chunk of code is to understand (our little brains are worst at holding several things at once, like Valentine’s Day and a wedding anniversary). There are other settings, such as nesting depth no more than 3, and arity no more than 6, and so on. These lint constraints force you to think better at the level of function implementation details. In most cases, the same functionality can be expressed in different ways; the linter’s purpose is to impose constraints that force you to express a feature in a more sensible way.
For example, code I often write offhandedly:
data
|> cleanup()
|> prepare(opts)
|> Enum.map(fn {type, value} ->
case type do
"type 1" -> # blabla
"type 2" -> # blabla
_ -> # default bla
end
end)
can be optimized into:
data
|> cleanup()
|> prepare(opts)
|> transform()
def transform(data) when is_list(data), do:
Enum.map(data, fn {t, v} -> transform(t, v) end)
def transform("type 1", data), do: blaba
def transform("type 2", data), do: blaba
def transform(_default, data), do: blaba
This lowers the code’s complexity, raises its clarity, and at the same time greatly boosts extensibility — adding handling for a new “type 3” later is just adding one simple function, fitting the open/closed principle nicely.
Little tricks like this depend on understanding the language and strictly abiding by the linter’s rules. Exceptions do occasionally occur — a complex SQL query expressed in Ecto easily blows past ABC — but the vast majority of the time, keeping the rules benefits you. Every commit, passing the linter is like your soul getting pounded in a sauna by steam — unbearably painful — and after you grit your teeth and endure it, pushing the door open and stepping out into sudden, incomparable freshness: the clouds part, the sun breaks through, and you feel you’ve leveled up.