The Most Important Things They Don’t Teach in CompSci 101 (but should): Maintainability

I stumbled across a great article about clean code from Jeff Vogel over at the IBM Developerworks blog site, via a /. article. Even more valuable was the How to write unmaintainable code article, which appears to have grown considerably since I first saw a version of it back in college.

A simple web search can uncover a lot of articles about writing maintainable code. There have been several books written on the subject. And yet, I consistently find that code examples and tutorials, not to mention programming text books, very often break all or most of these rules.

To a certain degree, what makes code “maintainable” is a matter of opinion. However, it is pretty universally accepted that the vast majority of programmer-hours are spent debugging, fixing, and extending existing code. It’s very rare, even when working on an entirely new project, that you open up a blank file in your text editor and start coding. After the first few hours of the first day of a project, you’re working in an existing code ecosystem. Is it a crazy jungle with vines and savage beasts? Or a well-lit sidewalk with friendly pedestrians?

I think these assumptions are pretty safe to make for any programmer in any language:

  1. At least 80% of a programmer’s time is spent working with an existing code base (possibly one they just created).
  2. Debugging code is more difficult than writing it. (Or, problems are harder to fix than to create.)
  3. Extending code (adding features) is the riskiest thing that you can do (bug-wise.)

Nevertheless, no matter what code you’re working on, every programmer faces these demands:

  1. Management allocates more time for development than for maintenance. (Usually after asking us for insight!)
  2. Maintenance is sloughed off onto the least experienced developer in the team.
  3. The existing feature set is looked at as a “base,” and new features are requested constantly.

It is thus impossible to overestimate the importance of writing code that is as easy as possible to fix, extend, and reconfigure, even if the person doing the fixing or extending has never seen the code before. (Or, as is perhaps more common, was the code’s original author, but has forgotten what they were originally thinking when they wrote it.) If you are a programmer or manager of programmers, look at it this way: at least 80% of your development budget depends on the maintainability of the code that you and your team produce. The techniques that create maintainable code are mostly free, but not using them costs you big time.

As a web developer, I tend to have a somewhat extended view of product lifetimes. There is still code on the internet from 1993, and that was back when we didn’t know what we were doing. Now that we’ve got all this experience, we should expect that the code we write will be around even longer. The programmer who gets to maintain your code might not have been born yet. Be kind to that poor soul.

These are the tips that I think are the most important contributors to writing maintainable code. It’s a matter of opinion, to a certain extent, and there’s plenty of disagreement, so take this for what it’s worth. Feedback is, of course, always welcome.

1. Don’t be clever

We already know you’re smart; prove it by writing code that monkeys can understand. Programming should be approached like a haiku, not a riddle. Make it simple, and don’t make me have to think hard when I come by years later to fix or change or add something.

Brian W. Kernighan famously said, “Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.” Try to find the balance between hard-coded and over-engineered.

2. Think clearly.

You can’t teach someone to think in an organized fashion. The best that you can do is find someone who does this already and convince them that they should do it when they write code. Create objects that make sense for the task at hand.

That makes it easy to:

3. Let the code describe itself

Comment your code, yes, yes, you hear it all the time. But don’t write comments that say how something is done. Write comments that say why, and let the code speak for itself. The only thing worse than this:

// increment i by 1 i += 1;

is this:

// increment i by 1 i += 2;

When you write comments that describe how instead of describing why, over time you inevitably end up with comments that lie, because code is much more likely to change than comments. Let the code say what it does. You just have to say why it does what it does.

4. Hungarian notation is eviler than eval

I sometimes think that Hungarian Notation was surely developed by some kind of mad scientist with an extreme hatred of maintenance programmers*. It’s almost never consistent, it’s hard to read, and it puts information where it doesn’t belong. It is a profoundly flagrant violation of the rule that comments should say why and not what.

Consider the basic case of “iCnt” for an integer counter, or ‘sType’ as a string, or 'oElement’ as an object. If your code is so convoluted that a maintenance programmer doesn’t know that your counter is an integer, or that the element reference is an object, or that “type” is a string, well, there’s something deeply wrong with either your code or your hiring process.

Avoid Hungarian notation like the plague.

* This is a joke. I’m sure it was actually developed by very well-meaning programmers who were honestly trying to address this problem. Nonetheless, it’s a very bad solution that tends to compound the problem significantly.

5. Small pieces, loosely coupled

A lot of maintenance checklists and lint programs instruct at the expression or syntax level. However, arguably the most important key to creating maintainable code is to build small pieces that interact with one another in loosely coupled ways. This minimizes dependencies, reduces the amount of code that you have to write and maintain, and makes extension and re-use much simpler.

Less code is almost always better. Instead of having 1 object that implements 10 features, it’s often better to have 10 small objects that implement one feature each, and just listen for and respond to the events that they need. Using a good event utility, this is an extremely powerful pattern for user interface code, especially. Widget-A just listens to an event from widget-B; when that event fires, it does its thing. If the event never fires, then widget-A never gets activated. This means that you can remove or change widget-B without fearing that you’re going to cause massive regressions.

Coding Horror is one of my daily reads. If you don’t read it, you should. Jeff Atwood put it quite well when he exhorted developers to Code Smaller. They he later wrote that the best code is no code at all. I couldn’t agree more.

6. Use OO (and other patterns) Wisely

Object Oriented programming can help you follow these rules. But, not everything has to inherit from something else. There is such a thing as over-engineering. “Silver bullets” approaches are common causes of this problem.

Use an Object Oriented approach that makes sense. Most of the time, in my experience, inheritance is not the best approach, unless you’re going to have a lot of types of things, where the types have a lot in common. This means that you’re dealing with a huge number of objects, which is not the case in most applications. Again, most of the time, “has-a” approaches are much simpler and more flexible than “is-a” approaches.

Think of it with a real-world example. “Sandwich” doesn’t inherit from “bread”. You wouldn’t say, “a sandwich is a type of bread that has stuff inside.” This might, arguably, be true. But, the way that most people talk (and more importantly, how we think,) is that a sandwich is a “thing” that has bread with stuff inside. Furthermore, a waiter is not a sandwich with legs; a waiter is a person who has a sandwich and brings it to you. Since said waiter may also have a second job as an actor, it’s not even right to say that the “waiter” class inherits from “person.” A waiter is not a type of person, it’s a person who has a job delivering food to tables.

Some languages implement multiple inheritance and other things that seek to address the “multiple hats” problem using classic OO techniques. If you ask me, don’t buck the epistemological trends—use the methodology that we use for everything else, and extend using has-a instead of is-a.

7. Be harsh on the code, gentle on the coder

We’re all human. We make mistakes. Even the best of us violate the rules sometimes, and most of us, by definition, are not “the best of us.” However, there is no excuse for bad code. It should be killed on sight. Be your own harshest critic, and never shy away from criticism. Seek it out. Turn to your coworkers and say, “Hm. You know, this thing I’m working on just seems like I’m making it too convoluted. Can I run my approach by you and see if I’m over-thinking this?”

Give honest criticism without any hesitation, but never ever say, imply, or even think that the programmer must have been dense. Sure, maybe the programmer really was dense. But if you’re growing as a developer, hell, as a person, then you should think the same thing about the stuff you did last year. Nothing is more humbling than having to support your own code a year later. Pepper your criticism with a light-hearted attitude, and be generous in praising the parts that you think are good. Keep your venting to yourself. It will raise your status if you help someone else succeed; tearing down just hurts us all.

Sure, this might be good advice, obvious, even. But how does it help make maintainable code? By fostering positivity, you’ll help create an atmosphere where bad code is not tolerated, but where everyone is encouraged to grow and experiment. The best way to avoid bad code is to make the programmers better.

I personally think that a few interpersonal communication classes would be a good addition to any computer science curriculum.

8. Fail

You won’t be able to create resilient systems unless you know how they’ll break. You won’t know how they’ll break until they do.

Failure is a prerequisite for success. So, get ready to do it. A lot. I’ve written LOTS of very bad code. (Some of it is even here on this website!) Plan for an alpha and beta. Do them as fast as possible. Bang on them. Then throw them out and start over, taking what you learned into the next version.

Share your failure. Have a code review session where everyone brings in the worst code that they’ve written for the project, and solicits ideas for what went wrong. (It’s kind of like an AA meeting: Hi, my name is Isaac, and I wrote bad code. Hello, Isaac….)

It’s so often these days, especially in big corporations with lots of different teams, for each team to share their successes loudly, but sweep their failure under the rug. We’re all afraid of getting a bad review, but this virtually guarantees that all the other teams will fail in exactly the same way. Even in a small company with only one or two teams, sharing failure can result in an exponential reduction in errors.

Furthermore, sharing your own failures, and asking for criticism, makes it easier for others to take criticism when you point out bad code that they wrote. It is very difficult to separate “me” from “my code”. If you criticize one, the other might get hurt. Sharing failure helps to sever this connection, and allows everyone to associate and connect with the success of the product as a whole.

Building a few prototype/failure builds into your development process will also help to produce stable builds faster. It allows for user-research sooner. By planning a throw-away build, it helps get everyone focused on which features are actually important/necessary, and which are either too difficult or unimportant to bother building. It gets product and engineering talking sooner.

Failure (and handling it intelligently) is not just important, it’s absolutely vital when it comes to building systems that are going to stick around for decades.