Wednesday, January 14, 2015

Why I write horrible code. (And so can you!)

EDIT: I may have been too subtle.

Some readers think this is a list of excuses for writing bad code. It is not. Instead, I want to analyze the reasons I have written bad code in the past, so that I can look for ways to make future code better. I want to acknowledge my own limitations, so that I can find ways to compensate. I also believe that many programmers have similar challenges, and may be able to learn from this analysis. Furthermore, I hope that by hearing about my imperfections, you can become less afraid of sharing yours, and that can open up opportunities for your growth.

Today I overheard a friend say something like "Who would write code like this? How could they think it was a good idea?"

I've written a lot of bad code, which makes me a kind of reluctant expert on the topic. It's possible that I'm just worse than average, but I've seen some great programmers write bad code too.

Here are the reasons I can see:

  1. Expediency

This is the most common reason that programmers cite. As in "I could spend some additional time to make this code more beautiful, but we need this change right away." 

I agree that the value of our work is time-sensitive, so delivering it sooner is better. And I agree that we are not being paid for the beauty of our code, but only for the value delivered to customers.

However, encoded in that statement are certain beliefs, about the cost to make the code more beautiful, how much better the code could possibly be, the value offered by that better code, and the risks in getting there. I say "belief" because I think they could vary by programmer, project, technology, market, organization, etc. I'll try to cover these beliefs as I go.

  1. Good design is unfamiliar

While all programmers have suffered from poorly-designed code, well-designed code is all too rare. We may know what we hate about this code, but we have a hard time knowing what "great" would look like. My college professors talked about "low coupling and high cohesion", but that conversation was always in the abstract - I didn't know how to make sure my code actually had those attributes.

I've often thought I knew a great design for something, only to discover that I missed many important details. If I ever get my code to the point where I can use it, I have compromised the design so much that it's not the huge win I was hoping for. I believe most programmers have had similar experiences. This feeds back in to the belief that attempting to make code beautiful won't give much return.

  1. We don't know what we need yet

When I start on a programming task, I usually have a bunch of questions I can't correctly answer yet:
    • What does my customer really need from my program?

    • Will the feature I have in mind really meet that need?

    • What is the true behavior of the externals I intend to depend on? (Do they have the capabilities I need? How do I call these APIs correctly? Do they scale? Are they reliable? Any bugs that will sting me?)

    • What is a good design for my code, based on answers to the above?

    • What future work will be difficult because of design decisions I make now?
Whatever I write, I will soon discover that I was wrong about my answers to these questions, and my design is no longer well-suited to the new answers. If I worked hard on that design, that hard work is wasted. If I work hard to revise the design, I may discover tomorrow that my new answers are wrong, too, so the revised design is also waste. This means I should take shortcuts to get my work done and in use, so I can get that feedback sooner and more cheaply.

Of course, when I get finally get the feature right, customers will not be interested in paying me to go back and rewrite it for no reason.

I used to think this meant that instead of working on good designs, I should learn how to work in poorly-design code, getting great at analyzing it in the debugger and finding minimal fixes. Now I know how to refactor.

  1. We don't know how to refactor

One time you tried to clean up a mess in the code, and you broke something. Your boss yelled at you. Customers were unhappy. You had to work extra hours to fix things up. Now you're wiser, and when someone says "I want to refactor this", you say "only a little, and only if you have great tests, and only if there's plenty of time." Which means it seldom happens. So we don't get any practice refactoring.

But refactoring is key: if you don't know what good design looks like (in general or specific), then the only way to get a good design is to start with a bad one and refactor your way to good.

More generally, remember that it's up to you to invest in your own skills. Refactoring isn't inherently slow or risky, but learning refactoring and other skills takes time and temporarily reduces your performance. You can't count on your employer to cover that, but it still matters.

  1. Too-big steps

Suppose you decide to clean up that code mess, once and for all. Part-way though, you get in interrupted. Maybe the live site goes down and you have to fix it, and that eats up the rest of your day. And tomorrow you have to work on some important new feature. By the time you get back to the cleanup, much of your work is no longer valid.

The antidote is to work tiny and get done. Do the smallest cleanup you can, check it in, and get back to work. Don't aim for "good", just for "better". Make things a little better each day. See Two Minutes to Better Code.

  1. We don't know what we're missing

So you're a smart programmer. Fueled by caffeine and isolated by headphones, you can get your job done. The code you work in is a mess, but you're still delivering value. Sure, you wish the code was nicer, but how much difference would it really make? Is it really worth the investment?

If you're only accustomed to working in code that is a mess, you're in no position to make this judgement. I know that is hard to accept. Really well-designed code doesn't just make things better; it makes things different. Ways that just aren't visible from the old way of doing things. For example:
  • No need to track bugs in a database, because there are no bugs.
  • No need to keep a list of future work (product backlog), because you can just pivot as needed.
  • Easy to test everything with super-fast unit tests, because everything is appropriately decoupled.
  • Ship at will, because you can verify ready-to-ship in a matter of minutes.
  • Any complexity in the code indicates an opportunity to reduce essential complication, since there is no accidental complication. (See 7 minutes, 26 seconds for definitions)
If you've never seen this it sounds impossibly far-fetched. A pipe dream. So of course you wouldn't invest the effort required to get there. (You probably believe that most of your code system complexity is essential; you're wrong again. Sorry.)

  1. We incorrectly compare short-, medium-, and long-term impact

Code mess creates a drag on development. As development gets slower, pressure increases. You take a shortcut. The mess gets worse. A vicious cycle. Exponential growth of the mess. (See Nobody Ever Gets Credit for Fixing Problems that Never Happened.)

In the (very) short term, we can deliver value sooner by taking shortcuts.

In the medium term, we will deliver features more slowly. Less value to customers = bad business.

In the long term, the cost of new features is so great that you must throw things away and rewrite, which you should never do. This isn't "pie in the sky" thinking; this is "we want to stay in business for more than 5 years".

  1. We don't ask for help

Even when my programming is going really well, as soon as another person sees my work, they'll notice a problem that I missed. Each person can offer a different kind of insight in to the design. I can learn a lot from that.

So turn that dial up, from code reviews, to pair programming, to mobbing.

  1. The code is just too horrible

How fast you learn something is heavily dependent on how fast you can iterate.

If you don't know what great design looks like, and you're not already good at refactoring, and your code is really really horrible, and your build takes forever, and your tests are crap, then every step you take will go extremely slowly.

If this is your situation, you could practice your skills in side projects and code katas, or you could switch jobs. Develop those design and refactoring skills in a better environment, then come back to this legacy code when you're ready for that challenge.

No comments: