Sunday, March 25, 2012

How do you know you have well written code?

If you’re lucky, in your career as a software programmer you will be called upon to write a whole new product—something you get to design from the ground up, with a chance to decide the variable names, class organization,file names, etc.

When you start it will be clear where you want to go and what you want to do. It's all shiny new code and your hopes will be high for the perfect software system.

Chances are, the first couple of times you do this you'll end up with the proverbial Gordian Knot. All your ideas for a clear design will get bogged down in implementation and plans that you made will end up in ashes.

It takes practice to write a good software system. You don't get good at it by maintaining software: creating something from nothing takes an effort and experience.

It's easy to recognize when you are working on good maintainable code. It's easy to work on. Easy to figure out where the fix should go. When you fix something it stays fixed and doesn't cause other problems.

These same things should be your goals for a new software system.

Clarity

Good, maintainable code has a clarity of thought that is so easy to understand that you forget someone had to work at it. While maintaining it, you don't have to think about why the designer did things a certain way because it feels obvious, it feels like there is no other way to do it.

Bad code has you scratching your head and asking "Why?" continuously as you look through the code.

That doesn't mean the good code won't have areas that need to be clarified. It does mean that every line of code and every subroutine doesn't require extensive documentation to justify its existence. If your code has been broken into subroutines artificially or without regards to any cohesive logic, then it’s hard to follow why it was broken along those lines. Since the goal is to write code that is easy to follow, creating artificial and illogical divisions makes the code difficult to maintain.

I advocate a top-down approach. I like to make my top-level modules and functions as clean and clear as possible. I keep pushing the implementation details down into subroutines until clarity is achieved. It isn't done in one throw: you don't know everything you need to know when you first start writing the code. You take a stab at it, then come back and comb through it, smoothing the bumps and jaggies until you get to an equitable arrangement.

One of my favorite software engineers always said to write the system, then throw it ALL away and start over from scratch. Everything you learned from the first try can be applied to the new system. By throwing the first try away completely, you don't tie yourself to fuzzy implementations. It's tempting to reach back and grab some stuff from the first implementation but it works best if you fight that impulse. If you start grabbing code from the original implementation then you start borrowing the structure and fuzziness that was created before you totally understood your problem. The more code you borrow, the more like the original implementation the code becomes, defeating the point of rewriting the code from scratch.

Lack of Problems

It's easy to recognize problem areas in the code. Those are the areas that you keep coming back to over and over to apply fixes. Every time you fix something, two more things pop up.This is a sign of code that desperately needs to be rewritten.

One technique is to “black box” that code. Pretend you know nothing about the inner works. Analyze the old code, isolate the inputs and the outputs, then rewrite it from the ground up using none of the old code. If your analysis was right you are likely to have fixed the trouble spots.

If the entire product is like this, then it's time to rewrite the product. It doesn't have to be done all at once; iteratively works too. Isolate an area, figure out the inputs and outputs, and rewrite from the ground up. What you learned by maintaining that code will help you in the rewrite. First you have to understand what the code was doing, why it was doing it, and why the approach that was made initially didn't work.

Help out the future programmers

It's fun to write throwaway code. We do this in college many times. Quickly write an application, ignore the error handling and any edge conditions. Stuff it in the computer and turn it in, never to look at it again.

This doesn't work in the real world. The real world is full of edge conditions and most of your real world code is error handling.

What you write today you might be maintaining 5 years, 10 years from now. If you make it hard for yourself by ignoring error conditions and edge conditions you will loathe yourself years from now. Consider what a stranger attempting to maintain that same code will think of you: after a few rounds of maintenance your IQ will get lower and lower as they struggle to handle the stuff you blithely ignored when you started.

Think about that when you write new code. Handle ALL the error conditions. Have a structure that can deal with those errors. Don't assume that an error condition is unthinkable. Even if most of them are, the few that are thinkable will be the ones the customer finds first and every time. Ponder ALL your edge conditions. Edge conditions take the most amount of effort to handle—that last byte in the file, a message that is one byte too long. Figure out how to handle those things in the beginning and save the future programmers.

Give it to someone else

At some point you’ll have to test whether the code is easy for you to maintain because you wrote it, or because you wrote it well.

The only way to do that is to have someone else work on it. It takes some personal security and bravery to do this, and you will have to reign in your ego. Taking criticism from outside parties on your blood, sweat, and tears is hard. But it’s definitely worth it.

Accept the feedback because that’s what you are looking for. If a sequence isn’t easy for them to understand, no matter how blindingly obvious it is to you, then you have failed to communicate your purpose and architecture. No one knows what goes on inside your head. No one can ever know that. You need to come to an agreement between what you see and what people outside your head can understand. You need to “speak” in such a way that people with many different viewpoints understand you.

Once you receive that feedback, act on it. Change the structure so it fits with a more worldly view. Internalize that view until you can see it. Sometimes the feedback doesn’t work directly with the architecture, but you should be able to marry the two until the code makes sense to everyone.

Reap the rewards

I know I've written a good software system when I get the Snowball Effect.The Snowball Effect happens when you've finished writing the system and now you are enhancing it and maintaining it. If it was well designed then the design makes the new stuff easy to add. Full new features go in with minimal effort, sometimes in less than a day. A bug reported by a customer gets fixed in minutes instead of weeks. If you keep to your good coding practices, this gets faster and faster until the work you do seems like magic.

As you enhance and maintain the system, look for places that are causing problem, either from bad original design or because the evolution of the product strayed from the original design. Don't be afraid to rewrite to help those areas fit in better.

Aim for your own personal Snowball Effect.