Jump to content

  • Log In with Google      Sign In   
  • Create Account





There are many kinds of ugly

Posted by ApochPiQ, 16 October 2011 · 5,653 views

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.


Spolsky's Bakery
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.


Accelerated Decay
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!




Programmers seem to get pretty hung up on elegance at times. I suppose it is a matter of pride more than anything to have elegant code that solves a problem vs ugly code that solves the same problem.. especially if nobody else can bear witness to how awesome the elegant version is.

I think of programmers of 20 years ago, who had to work insanely hard to pump out every last bit of performance to achieve a task quickly - but these days, the bulk of application programmers can get away with shitty inefficient code simply because computers are fast enough to handle it.

What's better, being done or being elegant? And for that matter, if nobody notices a performance difference than what does it matter if one "hacks" together a program vs takes a ton of time to do a beautiful masterpiece of elegant code that nobody will see or care about? The answer is probably just situational, but I'd bet that elegance is unimportant for a lot of code.
Michael, you are wrong. It's not really about how something gets done, or about pride. It's about how something can be maintained.

I think a lot of software gets started as a "quick hack, because it works". But as soon as you want to extend your code, or as soon as the customer asks for a new feature, you realize that the current structure of your code just "doesn't work" to quickly respond to changes in requirements. So you start to reorganize it and to make it cleaner, in order to make it easier to maintain.

Clean code - or at least a clean structure/architecture - is essential for the maintenance and extension of software. Clean code is easy to maintain and read. If you got a mess in your source code, it can cost you a lot of time (and therefore money), just to find a little bug and fix it.
I'm in agreement on all of this, although I will add that I'd rather work with messy code than code built by architecture astronauts (to use another spolskyism). I think you hit on an important point that some problems are just naturally messy, and trying to work with code that uses excessive abstractions to hide that fundamental truth is a lot more irritating than code that's just naturally messy but at least explains why it has to be messy. That's not to say I'm not in favor of good design or finding abstractions that suit a problem, but sometimes people read Design Patterns and start seeing a lot of nails for their new shiny hammers..
Great entry!
I will say that not all code is the same and shouldn't be treated as such. If you're hacking away at the architecture/framework code, you've got serious problems. Hacking away at a module that can later be replaced by another module is completely different and leaves plenty of opportunity to cut corners if necessary. Some code should be near perfect and this investment should be made from the start. Expecting all code to be pretty will only lead to wasted time.

@Facehat: I was very much guilty of this after reading Gang of Four. Everything had to be meticulously engineered. I think this was the one point in my life where I accomplished absolutely nothing. Reading a book on design patterns may certainly be a good first step, but it's useless without a great deal of practice.

Clean code - or at least a clean structure/architecture - is essential for the maintenance and extension of software. Clean code is easy to maintain and read. If you got a mess in your source code, it can cost you a lot of time (and therefore money), just to find a little bug and fix it.


I agree with this 100%. Knowing when to hack and when to be elegant is situational. Maintenance of your code definitely gets complicated when you hack something together.. and large projects fall apart when the foundation of code is pretty weak. But we also live in a world driven by market forces, so being able to get something done quickly can also be important. Your customers don't care if your code is elegant. So it IS about getting something done, because customers pay you to get something done. If you know the scope of the project will extend far into the future and it's well funded, then of course you're going to do a good job creating something that is maintainable.

Every situation is different.. but you can't make a blanket statement that clean structure/architectures are essential.. because they're simply not. It's very easy to get caught up in over-engineering something when you are a programmer.. accounting for situations that are unlikely going to even be issues in the name of architecture and elegance.

I suppose it is a matter of pride more than anything to have elegant code that solves a problem vs ugly code that solves the same problem.


Sadly, I would say this is often not the case. More often I see programmers who are bad, or at the very least beyond their skill level, solving a problem the best that they can. But due to lack of experience, or the lack of learning lessons from their experience their best isn't able to solve the problem as elegantly as possible (especially with the benefit of hindsight).

Or it's simply code rot. Something starts out elegant for the problem at hand, but 5 revisions later, a lot of those elegant solutions look ugly given the new requirements or technology at hand.

Pride I think factors relatively little in the elegance of one programmer's solution to a problem versus another's.
Code can be written to serve many purposes.

1. It solves the immediate problem (requirements).
2. It's easy to test.
3. It's easy to reuse and extend.
4. It's easy to read and maintain.

The priority of the above list is important but can change for each bit of code. For example,
- most of the time meeting the requirements is going to be high on the list
- code in a large project might put testability first because automatic tests are run everyday on a build server. (TDD)
- reusablitly and extendablilty is important in projects that have rapidly changing requirements (this is a big topic)
- readability and maintainability is important when you have many programmers working with the same code.

Lets say your requirements are to draw a circle on the screen. You have a very basic 3rd party graphics library that only knows how to draw lines on the screen.

On the first cut you might have a Circle class that has a Radius and Centre. The class could have a Draw method that you pass a reference to the 3rd party graphics library. The Draw method tells the graphics library where to draw the lines to form something that looks like a circle on the screen.

Unfortunately, while this approach solves the immediate problem, it's not very testable because automated test can't look at the output on the screen (technically you can do a screenshot test but that's another story).

One way to solve this problem is to put an interface in front of the graphics library so that the automated test can implement a "fake" graphics library in it's place. That way, the test can simply check that the expected lines are coming out of the Circle class.

Next up, new requirements say you need to now draw Squares and Triangles. At this point you refactor you're code so that they all inherit from a base Shape class and can be put in a loop to call the Draw method on each. Doing this makes future changes like this a breeze because none of this code needs to be changed again.

What's even better is that you can be confident you didn't break your existing Circle class during the refactor because you have automated tests that make sure it works the way you originally intended.

One day your boss decides they don't want to pay the license fee on the graphics library anymore because there is a free one that works even better. It just needs to be integrated into the existing code. Luckily, you put the graphics code behind an interface because now all you have to do is reimplement that interface with the new graphics library and none of your Shape classes need to change.

And so on.. and so on..

The point of the story is that you can get away with just meeting the requirements when the project is small but as it grows and requirements change you should constantly refactor the code to improve the design. Each refactor actually makes the code more resiliant to change and automated testing makes sure it still works.
I think many programmers are just stressed by tight deadlines.
They try to solve the immediate problem using the structure at hand, without taking the time to take a step back and think about if the structure needs changing or addition.
That leads to ugly hacks that "get things done" before the deadline, but the longer this continues, the harder new additions will become, and you start to lose time.

It's important to frequently force yourself to "think outside the box", and see if the problem can't be solved more elegantly with a refactor.
Often just a slight change in how you think about the problem will simplify it greatly.

And its important to have clear and thought through code guidelines so all developers feel confident in how to write readable and well structured code with consistent style over the whole project.

December 2014 »

S M T W T F S
 123456
78910111213
14151617181920
21 22 2324252627
28293031   

Recent Entries

Recent Comments

PARTNERS