There are many kinds of ugly
As I've gained experience in the software development world, I've moved through several stages of perspective on how code should look. In the early days, it wasn't even an issue; most code was equally mysterious and inscrutable, and if it did something cool, well, that was great. It didn't bother me in the slightest to do "evil" or "ugly" things, because the ultimate goal was to get stuff done.
Later on in life, I discovered that a lot of my laissez-faire "style" was actually cramping my productivity. All the usual suspects of code cleanliness started to jump out at me as bad things; that global really should be contained to that module over there, those two classes really shouldn't ever interact directly, maybe using a magic number instead of a clearly labeled constant really is a dumb idea, and so on.
So I went through an era of anal-retentive style obsessiveness, wherein everything had to strictly obey the Law of Demeter and may God have mercy on your soul if you even think about using a singleton. There was a brief - and I emphasize brief - flirtation with various flavors of Hungarian notation (I stuck with apps Hungarian the longest, but ended up eschewing that eventually as well). And things seemed good, for a time; my code was "pretty." Maybe, on a good day, it was even "beautiful."
Assault on the Ivory Tower
Something happened, then, though, that bothered me deeply for a while. I had to start writing production-level code, for real. Suddenly, I was forced to mar my beautiful creations with all this icky cruft: exceptional cases, sanity checks, assertions, handling of one-off weirdness... all manner of special cases and obscure trivia to deal with the realities of software that is exposed to general usage.
It reminded me of a famous military quote, which I decided to start paraphrasing as "no clean codebase ever survives contact with the customer."
I was salty - even bitter - about the affair for a while, maybe a few months, and eventually just sighed in resignation and gave up thinking about it. Maybe littering my nice, clean code with all this disgusting rubbish was just a necessary evil. And hey, if necessary evil pays the bills, well, call me evil.
There's a well-circulated piece by Joel Spolsky on this subject, wherein he draws a nice analogy to an industrial bakery. I'll butcher it in the retelling, so go find and read it yourself; it's an interesting bit of perspective. But since we both know you're not going to do that, here's the summary: there are many kinds of ugly.
What looks bad, even dirty or dangerous, to an uninitiated outsider can often represent the pinnacle of smooth and safe operation for someone who looks at the same situation with the right eyes. A little bit of paint flaking off isn't going to hurt anything, and that sheen of grease that seems so foul at first glance might actually be central to the operation of the machinery. It takes training and experience to recognize truly good, and separate it from truly bad.
All Jungles Look Alike
Unfortunately, there's a pitfall here. On the journey between seeing code as ugly and seeing it as necessary, there's a tiny divot of jadedness that can ensnare even the most well-meaning programmer. I've seen far too many people caught by this trap, and it is probably the number one contributor to code rot in otherwise well-run shops.
The problem is, one can start assuming that all ugliness exists out of necessity. There is no longer any distinction between code which is actually bad and code which just looks crufty because it has to be. A lot of production code is gross. It has to deal with all the obscurities and rarities of real life.
When you produce a product that is used by a million people, a "one-in-a-million" bug means you'll have a dozen reports of it on your desk by tomorrow morning.
So you end up with a situation where your code has to take into account some truly bizarre situations, because those situations - by sheer weight of numbers - are going to happen sooner rather than later. If you pride yourself on quality, availability, uptime, or any other metric of reliable and trustworthy software, you must consider these things - and consider them up front in the design, not as afterthoughts.
This leads to a mass of code that can appear pretty ugly. Threading assumptions, modularity concerns, security issues, and any number of other design constraints can lead to unwieldy-seeming architectures and implementations that start to resemble a plate of pasta more than an elegant program.
Fast forward this situation through several years of maintenance, upgrades, subsequent versions, employee turnover, documentation rot, and all the inevitable entropy of real products.
What you're left with is a mysterious glob of code that works - and maybe even works exceptionally well - but is incredibly hard to reason about. You can't just sit down and start changing stuff to make it look nicer, because you will violate some hidden rule that keeps the whole thing working smoothly. It can take a dozen times longer to understand the code than it does to actually make a new change, just because of the weight of complexity that has accumulated over the years.
Any good engineer in this situation will learn quickly that one can't just play around haphazardly in the name of elegance, cleanliness, or whatnot; the deep-seated truths implicit in the ugliness of the code are paramount. One does not screw with legacy code lightly, at least not without getting badly burned.
The more devious problem, though, is that most of your engineers will stop noticing the difference between code that is incidentally ugly - i.e. bad - and code that has to be ugly to get the job done properly. And this is where the trouble begins.
As engineers resign themselves to working in a morass of impenetrable code, they quickly learn that the idealism of elegance and beauty must be left by the wayside. Maybe you can recover some of that when writing new code, or strive for a glimpse of it when refactoring - but be sure your regression tests have good coverage, because otherwise, refactoring becomes a naughty word. It means breaking stuff that used to work so that the code looks pretty, and that's just not an acceptable tradeoff for any decent manager.
So refactoring gets deferred, and new code is mostly written in the style of the old code so that you don't inadvertently violate some undocumented assumption about how everything is meant to work, and before you know it, the codebase is rife with things that are truly hideous.
But you'll never notice, because the team has long since learned that production code has to be ugly.
A Solution to the Dilemma
On the plus side, there is a way out of this trap. On the minus side, it's excruciatingly hard.
The exact methods will vary, but the bottom line is twofold:
- Whenever you introduce necessary ugliness, document it
- Whenever you find ugliness of any kind, understand it
The second part is made orders of magnitude more efficient by religiously observing the first.
But the real key is taking time to understand your ugliness. If it's there because it has to be, go document it (if it wasn't already documented). This will save everyone a lot of time and brain-power down the line.
If you find ugliness, understand it, and discover that it is incidental - that it's just gross because everyone is accustomed to looking at gross code - you now have an opportunity to improve upon it.
This requires extraordinary amounts of discipline and work. It won't be something you can just inject into your daily routine, spend ten minutes a day on, and poof - ten years later your code is all beautiful. Doesn't work that way, sorry.
This is a fundamental change in cultural perspective. You have to actively fight the urge to just accept ugliness, but you have to do so carefully - because you must never forget that production quality code is almost always ugly. It takes time, leadership, and nearly universal buy-in; but it can be done.
The Road Goes Ever Ever On
This is where I am in my journey of perspectives on code cleanliness and beauty. I can't pretend to think that it's the end of the line, by any means, but it certainly represents progress over where I've been in the past, and that is enough to make me happy... for now.
I'd like to see this perspective spread, but I'm unfortunately not much of an evangelist, nor am I really in a position to make it happen by fiat. But I suppose I can put my thoughts out there, watch them travel a ways, and try to be disciplined enough to eat my own philosophical dog-food.
Maybe in a few years I'll be able to come back and reflect on where this curve of the road has taken me.
Guess there's only one way to find out!