
The first few months of my professional programming experience were mostly spent on old codebases. As many programmers can attest, it’s not unusual to find old source codes that reeks of code smells and littered with anti-patterns, making them frustrating to test and maintain. Sure, I could have refactored and cleaned up the code. But that’s not how my manager—nor the software users—measures the value of my work.
In the race against deadlines, I trudged through the code smells and anti-patterns before finally delivering the new features and bug fixes on time. The lingering anti-patterns, however, remain—a decision I know would vex future maintainers as it had vexed me.
Stopping this vicious cycle seems deceptively simple: just refactor your code! Yet, why aren’t we doing it as often?
Three Reasons Why
While most programmers are eager to write code, few are eager to refactor code—a sin I’m also guilty of. As discussed below, I think there are a few salient reasons for programmers’ reluctance to refactor codes (and it's not because we're lazy).
Broken Windows
Imagine that a group of delinquents have recently started throwing rocks at the windows of a nice and cozy apartment.
The first few broken windows may initially raise voices of concern amongst the tenants. If the police or the people who live in the apartment weren’t too keen on stopping the petty crime, the delinquents might continue to break more windows—after all, it seems that “no one cares.”
Frustrated, some people may start to move out. Rents may dwindle, the price of the apartment may drop, and the upkeeping costs may incessantly increase. Unwelcome idlers, loiters, and vagrants may eventually take their place. Those who stay are becoming disaffected and are normalized by the growing disorder, which, in effect, lowers the average “accepted” civility level of the community.
Over time, the voices of concern slowly turned into silence and indifference. As the apartment grew more dilapidated, abandonment soon exponentially followed. If only we’ve nipped the delinquents in the butts—as the old wisdom goes.
This is the core idea of Broken Windows Theory in criminology, and I was first introduced to it in The Pragmatic Programmer when the authors drew a parallel of the theory with refactoring.
According to The Atlantic Monthly article titled Broken Windows which first introduces the theory1, the “no one cares” behavior of the community—in response to the vandalism (or lack thereof)—gives off signals that can lead to the breaking down of the mutual sense of regard and civil obligations. The authors wrote:
[V]andalism can occur anywhere once communal barriers—the sense of mutual regard and the obligations of civility—are lowered by actions that seem to signal that "no one cares."
The lowered “communal barriers” lead to a downward spiral of a vicious cycle where less serious crimes begets the more serious ones, as described in the same article:
[…] A stable neighborhood of families who care for their homes, mind each other's children, and confidently frown on unwanted intruders can change, in a few years or even a few months, to an inhospitable and frightening jungle. A piece of property is abandoned, weeds grow up, a window is smashed. Adults stop scolding rowdy children; the children, emboldened, become more rowdy. Families move out, unattached adults move in. Teenagers gather in front of the corner store. The merchant asks them to move; they refuse. Fights occur. Litter accumulates. People start drinking in front of the grocery; in time, an inebriate slumps to the sidewalk and is allowed to sleep it off. Pedestrians are approached by panhandlers.
Back to our topic of refactoring: The temptation to ignore the problem at its onset is similar to how a programmer would ignore the signal of code smells during the early phase of its development. It is easier to say that we’ll fix the problem tomorrow or after delivering some important feature than to fix it on the spot when you’re racing to the deadlines.
In the programming world, we sugar-coat this bad habit with a term called “technical debt.” But we all know too well that as more of these debts are incurred (often at an exponential rate), we grow more reluctant to pay them. In reality, technical debt is a proxy for lowering the “accepted” level of code quality, not unlike how the neighborhood described in the Broken Windows article tends to become normalized with the lowered level of communal barriers. Indeed, each technical debt that goes unpaid over time amplifies the signal that says, “No one cares.”
The Price of Engineering
Even if we do manage to overcome the temptation and react early to the onset of code smells, we may face some difficulty in nipping the buds. How we make decisions to restructure and refactor our code—to fix the code defect as signaled by anti-patterns and code smells—can be positively influenced by our knowledge and expertise in the broader field of software engineering.
One tool that makes design decisions easier in software engineering is borne out of the realization that many problems—when we look at them at a high enough level—share some similarities that can be solved by similar design patterns. From refactoring.guru:
Design patterns are typical solutions to common problems in software design. Each pattern is like a blueprint that you can customize to solve a particular design problem in your code.
Since design patterns are established as typical solutions to common problems programmers face, most programmers in the industry should at least consider them when building software, right? After all, as the Gang of Four puts it: “Once you know the pattern, a lot of design decisions follow automatically.”
But not every programmer is willing to invest in learning design patterns when, realistically speaking, they can write away codes—while being oblivious to the “blueprints”—and still deliver working features. Why take the time to painstakingly learn the biblical Design Patterns by the Gang of Four when you can write working code now2?
One superficial answer is this: In every engineering profession, having the appropriate degree of engineering knowledge is critical for an engineer to observe the fine details that are easy to miss—to know that something’s not right—and, consequently, to have the know-how to make things right.
That may sound like a truism.
But when you observe the rise of a phenomenon such as resume-driven development—where narrow expertise on specific tools is given merit for hiring over “good taste” in software design—it seems that the superficial answer above may not be as evident.
The recently hired “Python API developer” may have done their job by the job description; after all, they have developed a web service with an API using Python. But does it scale to infinity3? Does the software design conform to the SOLID design principle? Do they design the application so that it is easily testable4? Clearly, there are many more abstract engineering factors that need to be considered when building any software—not just the language or tools with which the software is built. Design patterns are just one of them.
For software engineers, negligence to anti-patterns and the lack of necessary application of design patterns to combat them means that their source code—without clever refactoring to drive the positive evolution of the software—would eventually fall prey to software rot. In other words, being more knowledgeable in design patterns can help you articulate and reason about the anti-patterns and code smells plaguing your code, enabling you to develop a well-informed refactoring decision.
Of course, all these are necessarily shadowed by extra costs. The extra costs are the time spent not working on writing codes to learn about software engineering concepts and money spent to pay for the learning materials or training. Are these extra costs worth it? With a pair of myopic eyes, it may not be perceived to be so.
A politician may make a decision and enforce a policy that may sound good in the short term, just enough to rally voters to have a higher chance of winning the upcoming election. A programmer may also choose to divest from software engineering concepts and deliberations to quickly write away codes to meet the upcoming deadline, code quality be damned.
On both accounts, their choices can lead to consequences with terrible costs that only reveal themselves months or years in the future, perhaps after they’re no longer working on the project and have since moved on (perhaps to a higher position in the career ladder.)
As Thomas Sowell wrote in Basic Economics:
The fact that economic consequences take time to unfold has enabled government officials in many countries to have successful political careers by creating current benefits at future costs.
In other words, the fact that short-term benefits are easier to measure and observe than long-term benefits can be a boon to many politicians (and also—cough—to many programmers).
One of the examples the author used to illustrate this point concerns the problem of maintaining and replacing municipal buses, where it’s getting more expensive and costly. To keep the business afloat, the logical response for the bus service operator is to increase the fare price to cover the increasing cost of replacing the buses as they wear out. Yet the act of opposing such an “unjustified” increase in bus fares (as the politicians put it) by putting a price cap on the fare may be well received by oblivious bus riders. While this can, in the short term, reduce the cost of travel for many bus riders, it can increase the costs for travelers in the long term as the buses wear out throughout the years.
On the long-term consequences of force-capping the price fare, Thomas Sowell wrote:
It may be some years before enough buses start breaking down and wearing out, without adequate replacements, for the bus riders to notice that there now seem to be longer waits between buses and buses do not arrive on schedule as often as they used to.
When you also take into account the compounding effect of the Broken Windows theory, the overall economic penalty for the affected community can be quite high.
Using the municipal bus analogy, refactoring is the “price” increase the business has to pay to the engineers to cover the increasing maintenance “costs” thanks to anti-patterns and code smells. Due to this seemingly higher price of writing software than what would be otherwise with code that isn’t being regularly refactored—and compounded by how hard it is to measure the long-term benefits of such undertaking versus the allure of short-term benefits—it is easy to dismiss refactoring as unjustified extra work, as politicians do with the “unjustified” bus fare increase to cover the increasing replacement costs.
What can make refactoring expensive is the fact that good refactoring decisions require a solid grasp of software engineering concepts and knowledge—both of which require more time and effort for the programmer to learn and implement. This is simply the price to pay if you want well-engineered source code that will be easier to maintain, integrate, and test in the long run than you would otherwise with poorly engineered source code. At the managerial level, this translates to more expensive labor.
In short, if you want to write code that can stand the test of time, pay the price of engineering now or regret needing to pay for more later. Unfortunately, in reality, the person who chose not to pay the price of engineering and the person who does have to pay more some years later isn’t always the same person.
Incentives and Information Asymmetry
Perhaps programmers (or their managers) perceive little to no value in refactoring. As previously discussed, it may be viewed as extra work that can get in the way of delivering users the features or changes they want. Some, perhaps the clients for whom the software was developed, may even remark: Why bother fixing codes that aren’t broken?
Steven Levitt and Stephen Dubner, in their book Freakonomics, wrote that humans are driven by incentives:
Doctors, lawyers, contractors, stockbrokers, auto mechanics, mortgage brokers, financial planners: they all enjoy a gigantic information advantage. And they use that advantage to help you, the person who hired them, get exactly what you want for the best price.
Right?
It would be lovely to think so. But experts are human, and humans respond to incentives. How any given experts treat you, therefore, will depend on how that expert’s incentives are set up.
These incentives that influence the experts can work in your favor or against you. Car manufacturers, for example, are incentivized by designing better aesthetics, safety controls, comfortability, and durability of their cars to attract prospective buyers or risk losing their customers to their competitors (in a free market economy, of course). On the other hand, real estate agents may sell your house cheaper than they would their own house to sell your house quicker, and in doing so, they would lose only a meager couple of hundred dollars in commission compared to you, who potentially could lose a couple of thousands. (You will probably sell your house faster, though.)
Programmers—as their clients are often led to believe—are experts in writing software that delivers features deemed valuable to them. If their clients want some fancy features, the programmers are incentivized to do just that: Write codes to deliver the fancy features, often sacrificing best practices and code quality to meet deadlines and secure project milestones.
As the project evolves (or perhaps even later after “go-live”), however, the source code tends to grow unwieldy: Integrating new features becomes increasingly complex, and bugs become harder to catch and fix. But the programmers were effectively inculpable—for they had lived up to their promise of delivering the requested features in the initial allotted time frame, and the clients were still no less oblivious about their unfortunate situation.
Here, we can see an example of information asymmetry: a typical situation where one party of a transaction has better information than the other. Naturally, the party with more information would be incentivized to withhold some information so as to maximize their reward or minimize penalty. Thus, while neglecting refactoring works may save the programmers some effort, unbeknownst to their trustful clients, they will have to pay the hefty consequences of hard-to-maintain software in the future (e.g., paying more for support and more costly maintenance). In other words, the programmers are rewarded with more work (and pay) to fix the growing problems thanks to poor design decisions early in the development cycle.
Interestingly, the example above may not hold true in the Software-as-a-Service (SaaS) model, where the client would make recurring payments—as opposed to the traditional model of paying for the software to be developed (usually by outsourcing) and run in the client’s own infrastructure operated by the client’s own team. In this case, the programmers are incentivized to maintain high-quality source code, motivating them to make changes, add new features, and catch bugs more efficiently, thus ensuring better customer retention and attracting prospective new subscribers. If they were to do otherwise, they'd risk losing their customers’ subscriptions, who would rather move to better SaaS providers. Like what Gregor Hohpe (an enterprise architect at AWS) had advised, don’t run what you didn’t build!
Moral of The Story
A good piece of advice is to refactor whenever you see the signals—the smell of code stinks. That’s when you know there’s some refactoring to do. At the very least, if you cannot refactor immediately, make a note of it—raise an issue or a backlog to highlight the code that needs refactoring. Other programmers in your team may not be aware that the code suffers from anti-patterns and can be further cleaned and improved. Don’t let that one broken window instigate more code smells being left unattended. It is far easier to refactor small pieces of code than to refactor a giant, big ball of mud.
Apart from the deterioration of code quality over time, the continuing shifts in the knowledge and available technology that went into the design and implementation of the software are another reason that refactoring is inherently a never-ending task. For example, gaining more clarity into the business domain may guide you to better design decisions5, as does learning about design patterns appropriate for your use cases. In addition, adopting new technology that wasn’t available even a few years ago might make your code a hundred times easier to maintain.
Alas, you can never really end the cycle. The vicious cycle will always be alive, and with each cycle, your source code can grow insidiously. It is thus crucial for you and your team to understand that refactoring is an essential, perpetual task; negligence of this fact would ensue further disarray and anti-patterns proliferating the source code—plunging you, your team, or future maintainers into maintenance hell.
The Real Moral of the Story
Perhaps it’s too easy to give such advice that puts the burden of change on the individual programmer. Instead, let us ask ourselves: Are we working in an environment where it’s easier to do the right thing than the wrong thing? Can we be incentivized to refactor code—an unglamorous and underappreciated work—so that we’re not only myopically focused on short-term rewards (i.e., features and bug fixes) but also on long-term values (i.e., maintainability, testability, and scalability)?
In Atomic Habits, the author James Clear wrote:
Environment is the invisible hand that shapes human behavior.
If we as programmers can’t change our behavior—to fix broken windows early, to pay the price of engineering, or to be more vigilant on code quality—by simply trying to be more proactive and disciplined, then perhaps it’s time to scrutinize the environment that shapes how we work.
According to the Wikipedia page on Broken Windows Theory, the article was from the March 1982 issue of The Atlantic Monthly, where this theory emerged.
While I appreciate the Gang of Four's contribution to bringing Design Patterns to light in the art of programming, I find the book too dry and boring. Its examples are also a bit dated by today’s standards. For the aspiring computer whizz kids in the current year, I’d recommend refactoring.guru.gra
On the initial conception of Amazon’s AWS, according to Amazon Unbound:
The business plan [of building a cloud service] was barely understandable to many of Amazon’s own employees and board members. But the forty-year-old Bezos believed in it, micromanaging the project and sending extraordinarily detailed recommendations and goals to AWS team leaders, often at night. “This has to scale to infinity with no planned downtime,” he told the beleagured engineers working on the project. “Infinity!”
See Clean Architecture, by Robert C. Martin (Uncle Bob).
The book Domain-Driven Design by Eric Evans is a good starting point.