One of the amazing things about working in a department with other coders is that you consistently see the same mistakes being made day after day, month after month, year after year. Whether it’s due to general timidity, fear of criticism being taken personally, lack of a formal review process, or a company culture that does not encourage reflection and improvement, coders are often hesitant to give constructive criticism to other coders. Without such criticism, coders often lack the opportunity to recognize and improve their coding skills, which leads to the same mistakes being repeated in the future. And mistakes and bad habits by one coder on a team ultimately end up costing everyone time, which is always the most constrained resource in a production environment. To minimize such bottlenecks, designing your code the right way the first time is paramount -- and that means having good habits.
The following common sense suggestions have been gleaned from years of experience working both at the professional level and on large-scale personal projects. This article is dedicated to my fellow programmers working at both the professional and hobbyist levels.
1) Plan for extensibility
This is perhaps the #1 issue that I run across on an almost daily basis. Generally when we write a piece of code, we are doing so in response to trying to solve a very specific problem. It is very easy to get stuck in the mindset of “here is the problem I have to solve, and here is how I am going to solve it”. Then a specific solution tailored to that individual problem is implemented, which works great in the short run, but ultimately leads to huge maintenance issues down the road.
Extensibility should always be kept in mind for any piece of code you write. Whether due to feature creep (features not originally anticipated), faster computers, or more sophisticated customers doing more complex things with your program, specific solutions implemented today are going to need to be extended to do something else tomorrow. For example, you may write a small library to handle working with BMP files. Tomorrow your customers are going to want JPEG support too. Today you’re writing certain information to a file, tomorrow there will be 3 times that information that needs to be written to file, and the old files will still need to work too. Your customers are using your software to do a task today, tomorrow they’ll want to do a task with 10 times as many fields, objects, or whatever unit is appropriate to your software. If your code isn’t already set up to support extension, you’ll end up bogged down in maintenance, and every addition will cause your software to become more complex and unmaintainable.
There are many simple suggestions that can help extensibility: Don’t code specific solutions -- code generic, reusable libraries. Don’t assume the length or size of anything will be constant. Version all your user-data files. Use encapsulation.
By far the most time spent in any stage of the software life-cycle will be spent in the maintenance phase -- so it is imperative to design your software in a way that encourages easy maintenance -- and that means designing for extensibility.
2) Don’t use magic numbers
Magic numbers are numbers that are hard-coded into your project, usually without explanation. They often represent explicit assumptions that are being made about the size, length, or value of something. Because magic numbers tend to be somewhat arbitrary, they often do not carry any meaning in and of themselves, which can ultimately make your code harder to read, understand, and maintain.
For example, you might see a piece of code that looks like this:
for iii = 0 to 15 if (slot[iii].type == 12) return true;
What do the numbers 15 and 12 mean in this context? Why were these specific numbers picked? These numbers do not convey any specific information to the reader about what is going on in this segment of code.
Ideally, magic numbers should be parameterized -- the mechanisms for doing this vary from language to language, but use of symbolic constants (via const variables, #defines, or enumerated values, for example) are much better choices. The above code, rewritten using symbolic constants:
for iii = 0 to MAX_INVENTORY_SLOTS if (slot[iii].type == TYPE_ITEM_POTION) return true;
Suddenly, this code makes a lot more sense. Without even knowing much about the rest of the program, it’s pretty obvious that we’re examining the inventory (probably of a creature), looking to see if there are any potions.
Furthermore, using symbolic constants instead of magic numbers aid extensibility. In the future, your character may want 20 inventory slots instead of 15. Using magic numbers, you’d have to search the code for all instances of the number 15, and change them by hand. Changing a symbolic constant usually involves searching for MAX_INVENTORY_SLOTS and changing the definition from 15 to 20. This can not only save a lot of time, but also prevent mistakes caused by inadvertently changing the wrong magic numbers. For example, consider this snippet of code:
if (iii == 15) return;
What does this 15 mean? Is it the maximum number of inventory slots (in which case you should change it), or does it mean something else (in which case you shouldn’t)? Without understanding the context of the code around it, it’s impossible to tell. That makes doing mass changes when you need to expand the number of inventory slots dangerous, as it introduces the error of inadvertently breaking other code that already works.
(Author’s note: using better variable names than iii is also recommended)
3) Document why, not what
Almost all coders intuitively understand the maintenance value of good comments. And yet, many programmers misuse comments in such a way that they add little to no value to the maintenance process.
Here is an example of a comment that does not add anything:
# Loop through all of the inventory slots for i = 0 to MAX_INVENTORY_SLOTS
It’s pretty obvious what the code is doing even without the unnecessary comment.
Here’s another useless comment that you sometimes see:
iii = iii + 1; // increment iii
Comments should not be used to describe what the code is doing. The syntax of the code tells what the code is doing. A good comment tells how or why the code is doing what it’s doing. How comments are often useful for outlining the algorithm that the code is using to solve a problem:
/* The following code scans the entire dungeon looking for spots where a hallway connects to a room. This done by examining adjacent square of the dungeon. If a square is a hallway square and the hallway square has one adjacent room neighbor, this is a candidate square. */ // Algorithm implemented here
How comments are best utilized at the top of a function or block of code, as they allow a programmer to understand whether the following code is relevant to their interests at the time without having to read and comprehend all of the code in the block. This can save a lot of time during the maintenance phase when trying to locate a particular segment of code.
Why comments are ideal as individual line of code comments. Why comments should explain why the coder is doing something specific:
// Because all creatures have been priority sorted by time, // if this creature (which is the next to move) has a time // greater than the current time, we can advance the // CurrentTime variable so that the creature can take it's // move. if (pCreature->GetTime() > CurrentTime) CurrentTime = pCreature->GetTime()
If your code really needs a comment to explain what the code is doing, your code probably needs to be refactored or simplified, not commented.
4) Don’t reinvent the wheel
Programmers in object-oriented languages are often tempted to write their own classes to do things which have already been solved a million times by other coders. This is particularly true with container classes. While writing your own array and linked list container classes can be a good academic exercise, or even fun if you enjoy that kind of thing, why do so when you can reuse one that’s already been written? This not only saves you the time needed to implement the class, it saves you the time of having to test and debug it, and future versions of the class may provide additional functionality without you doing anything other than install them. Furthermore, if the container class is part of the language (such as in C++’s standard template library), programmers reading your code will be more likely to already be familiar with how those containers work, reducing the time needed for new programmers to become familiar with the code.
Reinventing the wheel can also lead to code duplication, which makes your project more complex and more difficult to maintain. If you have two or three pieces of code doing the same task, you may need to modify all of them to enhance their functionality. This is effectively wasted time that could be used elsewhere. Furthermore, it leads to confusion over which version has what responsibility and makes finding errors more difficult (because it’s harder to isolate which code is being used for a particular problem).
One of the complaints heard about some libraries such as the C++ standard template library is that the interface is awkward. If that is the case for you, rather than rewrite a container that already exists, create a wrapper class and wrap the functionality you use most often in your own interface. Generally, the performance penalty for doing so is small, you can tailor the interface to your needs, and if you need to extend the functionality of the underlying library, you already have a convenient place to do so.
There are also plenty of good free libraries out there that may already solve the problems you’re trying to solve: as an example, SDL, FreeType, and WxWidgets are all examples of cross-platform libraries that may offer easy solutions to your problems. Even though you must learn how to use them, often the time needed do so is significantly less than the time needed to implement your own solution, let alone debug and maintain it!
Note: Be careful of licensing issues, especially if using open source code in commercial projects!
5) Work incrementally
One of the biggest mistakes hobbyist programmers make is to try and implement too much at a time without spending adequate time testing each section. As a result, bugs start piling on top of other bugs, not only making them harder to find and isolate, but also making the number of bugs needing be squashed seem unmanageable.
A much better idea is to work incrementally. After each section of code has been written, think, “How am I going to test this?”, and then actually test it. If you find any bugs, either in the code you’ve just written, or in code that your code is relying on, those bugs should be taken care of immediately.
Focus down on one area at a time. Work on one particular piece of code until it does what you want. Every time you switch between areas, your brain has to do a context switch, which makes you less productive and wears you out faster. If you’re writing a container class of some sort, don’t implement only the portion you need now and come back to do the rest later. If you know you’re going to need it later, implement it now, while the details are still fresh in your mind. It is truly amazing how quickly one forgets the details of what one has already coded! You’ll be less likely to make mistakes, and your testing plan will be more coherent and structured.
6) Find someone willing to criticize your work
As alluded to in the introduction of this article, finding someone willing to give you constructive feedback is one of the best ways to learn. If they are willing, this person should be consulted even before you start coding! Bring an outline of your proposed solution to them, explain it to them, and have them give you feedback on it. In the process of explaining it to them, you will not only solidify the problem in your own mind, but they will also be able to offer you advice on things that you may not have foreseen, such as potential problem areas, better algorithms that you had not though of, or where you’re intending to reinvent the wheel.
Once you have coded your solution, have someone take a look at it. They will more than likely be able to point out places where you haven’t made your code extensible enough, where you have used magic numbers, where you haven’t documented well.
Remember, on a team, everyone is in it together. When your teammate writes bad code now, it could be you that ends up having to extend it tomorrow. Consequently, it’s worth everyone’s time to work together to make sure good habits are utilized up front, so the penalties aren’t so severe down the road. If you’re working alone, having good habits will help keep your code structured and flexible, making it easier to work with and less trying on your patience.
Although much of the above seems obvious, these things are often shortcut in situations where there is time-pressure to get things done. However, these suggestions should not be thought of as preventative measures, they should be thought of as investments. Spend a little bit of extra time now, save a lot of extra time down the road. “But what if I don’t have a lot of time now?”, you ask. Do it anyway. Right now is already down the road from some time in the past, and as code ages, the more these kinds of mistakes tend to compound. Doing the right thing now will give you more time in the future to continue doing the right thing. Ultimately, that not only makes for a higher quality product, it makes for a happier programmer.
If you believe this article was worthwhile, please recommend it using the social bookmarking icons below. Many thanks, and happy coding.
- Eight C++ programming mistakes the compiler won’t catch
- Three questions that need to be answered before you start writing your game