It's Time to Think About How to Delete Code
In My Own Swordsman, when Scholar Lü goes toe-to-toe with Ji Wuming, he delivers a brilliant speech on “Who am I.”
Who am I? Where does my life come from, and where does death lead? Why must I appear in this world? What does my appearance mean to this world — did the world choose me, or did I choose the world?! Is there a necessary connection between me and the universe? Does the universe have an end, does time have a length? Where does the time of the past disappear, and where does the time of the future stop? Is the question I raise at this moment still the same question you just heard?
We rarely ask ourselves such foolish questions. Many things are so taken for granted that not only do we fail to see the question ourselves, but when someone else asks it, we answer with a pure and simple doesn’t look like anything to me. Take questions like these:
- Why do we do code review? Can we skip code review? Would skipping it cause serious problems?
- Within a team, why must we all write code in the same language? Why can’t programmers use whatever language they like?
With questions like these, merely thinking about them is politically incorrect, let alone asking them. The many processes we follow when writing code all ultimately serve one goal: better maintainability. After all, once written, code becomes a company asset for a very, very long time to come: we need to add new features in response to market needs, we need to fix discovered bugs and latent issues, and we must keep improving its efficiency and capabilities so as to reconcile the contradiction between the people’s ever-growing demands and backward productive forces — that is the principal contradiction in the primary stage of software companies. So we must guarantee maintainability — and code review, colleagues backing each other up, and developing in the same language are merely means to that end. Which brings us to the question:
Why must we maintain code? Why can’t we rewrite code? Or rather, why do we maintain code by modifying it, rather than by deleting and rewriting it?
There are many reasons why we can’t, or won’t, do this:
- Adding a new feature while guaranteeing no regressions is already enough to make your head spin — it touches dozens of files; the new code may be only a few hundred or thousand lines, but the code involved runs into the tens of thousands. Rewrite it all? Can you even write that much? Too risky.
- The change isn’t that big; no need to rewrite.
- Are you insane? Even if this were right, my boss would never let me do it.
- Rewriting my own code, fine — but rewriting someone else’s? For those better than me, I lack the nerve; for those worse than me, I lack the heart.
- …
Of these, only the second reason holds; the rest really don’t. The first is an architectural problem; the third and fourth are cultural, or rather cognitive, problems. And the architectural problem, if you dig into it, is also a cognitive one.
Because we have decided that the code we write is not throwable code, but maintainable code; because we never see code as a liability holding us back, but as precious sedimented asset. So we happily pile up millions of lines, day after day, never considering planned deletion.
The same cognitive problem shows up in imperative versus functional programming. A variable is called a variable because it can be assigned, can be modified. Many programmers cannot imagine how they would live if a variable could only be bound and never modified — because they don’t even know how to handle the most basic control flow: loops. Tell them about higher-order functions and tail recursion, and their face goes blank: it doesn’t look like anything to me.
You might say that functional programming and throwaway code are two completely different cognitive issues: the former is rational and backed by academia, while the latter is madness, close to nonsense. Let’s set that debate aside and suppose a madman really did ask us to design an architecture in which any new code we introduce can, at some future moment, be entirely thrown away and rewritten without affecting the rest of the system. How would we do it?
A spaghetti architecture certainly won’t do. Never mind pulling out some feature and discarding it without breaking functionality — merely pulling it out might already exhaust everyone’s strength.
So we must modularize. Each module minds its own business — render unto God what is God’s, and unto Caesar what is Caesar’s. That way, even if we abandon Caesar, God will not perish.
Modularization alone is not enough. We must also consider layering. Nuclei and electrons form atoms, atoms form molecules, molecules form cells, cells form tissues, tissues form organs, organs form organisms. Each layer minds its own business; a cell does not meddle in how the electrons in an atom make their bizarre quantum leaps. Layering ensures that within a changing system there can be unvarying, stable parts. In the ISO/OSI model, changes at the physical link layer are masked by the transport layer, and changes at the transport layer are neatly concealed by the application layer. When a programmer sends a GET / request, they don’t care whether it travels over IPv4 or IPv6, let alone lose themselves in the myriad of interface forms like RJ45 and fiber.
Layering is still not enough, because while it untangles vertical coupling, it neglects horizontal coupling. Take the simplest example — almost all web apps embrace MVC today, but almost every web app built on the classic MVC frameworks traps you in horizontal coupling: Rails, Django, Phoenix, all of them without exception. If you were asked to design a blog system, you would naturally start from Models, designing basic models like Blog / Post / Comment / User. Yet Post and Blog could be unrelated; Blog is merely a container for a set of Posts satisfying certain conditions. Comment likewise has nothing to do with Post; a collection of Comments satisfying certain conditions happens, by coincidence, to constitute some attribute of Post. So from the moment you type rails generate model, your code is already destined to have strong horizontal coupling, making it hard to delete and rewrite any single Model. One day, when you discover that Post needs to be stored in a data store completely different from User’s, you’ll find that this is nearly an impossible task — unless you rewrite the whole system.
So we also need to decouple function from function, that is, to service-ize, constraining each side’s behavior through interfaces or protocols. In the blog system above, Post should be a complete black box to User; User cannot touch Post’s internal state (what storage it uses), and can only fetch Post’s information through an agreed interface. That way, Post and User can live in the same database, or in different ones.
Service-ization can partly let us throw away some service’s code and rewrite it completely — so long as the interface stays unchanged, the rest of the system is unaffected. But it is still not the final answer. We set out from one question and have already come this far; we can be well satisfied. Stephen Covey counsels us: begin with the end in mind. If, when we start writing code, we can consider how it might later be more easily deleted, then we will design with more deliberation. And we will discover that writing code that is easy to delete and rewrite is far harder than writing code that is easy to maintain.
In his first hundred days of new policy, Trump mentioned one item: for every new federal regulation, two existing regulations must be eliminated. Whether or not this is even possible, it is a bold and farsighted idea. The reason society grows ever more inefficient has a great deal to do with regulations (administrative orders) that only accrue and never get deleted, or that grow faster than they are removed, becoming bloated and convoluted; the reason software systems grow ever more unwieldy and hard to maintain is likewise that we only keep adding and modifying code, that code accrues faster than it is deleted — and, as it happens, most of what accrues is business logic. Know that much of the business-logic-related code from years past may already be wholly out of step with the current commercial environment; keeping it only adds vexation while bringing little value.
For problems like this, the biological world offers an answer: metabolism. Metabolism is the exchange of matter and energy between an organism and its external environment, together with the process of self-renewal of matter and energy within the organism. Metabolism comprises anabolism (assimilation) and catabolism (dissimilation).
Anabolism is the process by which an organism converts nutrients obtained from the external environment into its own constituent substances and stores energy — through anabolism, the programmer, that chlorophyll within the software system, uses nimble hands to convert the fruits of his painstaking mental labor into code and channel it into the system. Catabolism is the process by which an organism decomposes a portion of its own constituent substances, releasing the energy within and expelling the final products of decomposition from the body — for the programmer, this means winnowing the chaff from the system, keeping the essence; yet we rarely employ it.
In writing this rather controversial (and not yet fully thought-through) short essay, I do not mean to say that code maintenance is unimportant, that refactoring is unimportant, and that only rewriting is the true path. Rather, I want to raise a question: is it possible that, at the very inception of architecture, we could consider that this code might become a liability, and therefore design with an eye to how it might be easily deleted?