Introduction
The success rate for software development projects around the world is shameful. Projects are very often delivered late or over budget, which is acceptable because the alternative seems to be not delivering the project at all. Certainly there are things in the development lifecycle that are out of the developer's hands, such as the quality of the requirements. However, one cause of poor software delivery success rates is that applications with large amounts of code can quickly become unmanageable, tangled messes that no one wants to touch. While the steps in this article aren't the only answer to these problems, writing maintainable code will help keep projects moving to completion and through the support phase.
Write For the Person Who Will Maintain Your Code
Unless you work for yourself, the code you write will likely be touched by someone else at one time or another. Just because you felt like you were looking for a needle in a haystack when you were maintaining someone else's code doesn't mean you should force the next person to have the same experience. Instead, focus on things you can do to make your code more readable.
Variable/Method Names
I'm not going to advocate the use of any particular notation or convention. As long as the notation makes sense and is consistent, you're probably fine. What you want to avoid is changing notation dramatically during the course of development work. The reader should be able to get used to the notation, whatever it is, fairly quickly, and changing conventions mid-stream will only cause confusion.
One thing you should focus on is making readable variable, function, and object names. You might not think there's a problem with this:
Object s = this.GetSaxophone();
The problem is that if you have a long function, you will have to remember what "s" is. Instead of doing that, try this:
Object saxophone = this.GetSaxophone();
Now, no one needs to question what "saxophone" is supposed to be.
The same concept applies to method names. The method name should be a short description of the method's purpose, not some arbitrary set of letters. Also, since you are naming functions after what they do, you should ensure that your function names are verbs to reflect their purpose.
Short Methods
It can be difficult understanding what's going on inside many functions, but it's much worse when the function is 2000 lines long. If you're debugging such a monster, you're likely going to forget all of the assumptions that were in place when the function started. You're also likely going to have problems mentally breaking down the function into smaller bits of functionality for debugging. The solution is to explicitly break the function down into smaller ones, ideally groups no larger than your computer screen. (No, this does not give you an excuse to cram as much code into each line as possible.) Your parent function could call these child functions, giving the code reader an easy way to get a high level view of the code's purpose by looking at the parent function, while also allowing the reader to see each component individually by looking at the child functions.
Refactoring
Whenever you're making a change to code, you're probably either making it better or making it worse. If you're leaving code in that no longer works, or are working around code that may or may not work, or are leaving in code that works but has turned into spaghetti code due to all of the changes made, you're making it worse. Take the following example:
MySaxophone.DoSomething();
Let's add support for tenor saxophones:
if (MySaxophone is AltoSaxophone)
DoSomething();
else if (MySaxophone is TenorSaxophone)
DoSomethingElse();
By this point, you should be thinking about refactoring this code. What happens if you add a BaritoneSaxophone
type? How about SopranoSaxophone
or BassSaxophone
? If you're working in an object-oriented language, you should consider moving this method into the base Saxophone
object and override it in the specific saxophone
types when needed. Otherwise, you could run into a rather large if
/else
chain.
If you're thinking that this example isn't so bad, you're right. If your application is fifty lines long, having one awkward chain of if
/else
statements isn't going to make or break your application. When you apply this idea to an application that has several hundred thousand lines of code, though, it should be fairly obvious why it's important to keep on top of the little things before they become big problems.
Real Fixes vs. Quick Fixes
Related to refactoring, you as a developer should always be looking to fix the cause of the problem, not to fix the symptoms. For example, if an application blows up whenever it goes to the database and gets a string
when it expects an integer
, many developers I've encountered will trap the data and try to turn it into an integer
the best they can. That will make the application worse. The correct solution would be to find out why the non-integers are being put into the database in the first place. Is this the cause of the bug? Do you misunderstand what this field is actually being used for? Or is the actual problem something else entirely? Without the answers to these questions, you're at best creating additional confusing code for the next person to clean up and at worst causing more bugs than you are fixing.
Commenting/Documentation
You might be surprised to see that I haven't mentioned comments or inline documentation yet. Comments can be useful at times, but one should never write a comment to explain an unclear block of code without first making some effort to make it readable. Your goal should be allowing your reader to understand what you are doing the first time through your code. Use comments when they get you further towards this goal.
Keep Code Testable
There have been several studies done stating that the sooner an error is found, the cheaper it is to fix. Running the application and testing each possible scenario can be time-consuming, and regression testing large applications can be expensive and unpleasant. Fortunately, you can write methods to test your own code, helping to eliminate errors before they are found by testers or end-users.
Write Unit Tests
Unit tests are essentially bits of code that test individual components without needing to step through each line manually. Here is an example of a method that could use a unit test:
Decimal CalculateDiscount(Int32 age, Decimal amount)
{
}
Here are the skeletons of some potential unit tests:
public void CalculateDiscount15And100Test()
{
Decimal discount = CalculateDiscount(15, 100.00);
Assert.AreEqual(.10, discount);
}
public void CalculateDiscount40And500Test()
{
Decimal discount = CalculateDiscount(40, 500.00);
Assert.AreEqual(.12, discount);
}
public void CalculateDiscount70And1000Test()
{
Decimal discount = CalculateDiscount(70, 1000.00);
Assert.AreEqual(.15, discount);
}
Assuming for the sake of example that there is an error in the code that calculates discounts for 70 -year-olds, your unit test results might look something like this:
CalculateDiscount15And100Test: PASSED
CalculateDiscount40And500Test: PASSED
CalculateDiscount70And1000Test: FAILED
There are two important advantages to being able to test your code like this. The first is that you don't actually have to start up your application to test this method. If this were contained within an e-commerce website, you might have to log in as three different users and make three different purchases in these amounts to test the same scenarios, which would be very time-consuming. Unit tests are much faster to run. The second advantage is that these unit tests would continue to exist in your project (though preferably within their own container so the code wouldn't be pushed to production). They could be run periodically, which greatly reduces the need for manual regression testing.
A further discussion of unit testing is beyond the scope of this article, but entire books have been written on the subject. There are also multiple frameworks available in multiple languages to help you get started.
Write Once, Use Most Places
Another benefit to breaking your code into smaller pieces is that the components can be reused. Using the example in the previous section, if your application needed to get the discount amount in multiple places, you can simply call your original function in each place and can reasonably expect it to work. Having said that, don't take this to extremes. There are scenarios in which you may be tempted to push the limits of code reuse. Create new functions or objects when necessary to keep your code simpler.
Use Exceptions Wisely
Exception handling (preventing the user from seeing their application crash) is, in general, a good thing. Most undesirable situations, such as the user entering inappropriate values, should be anticipated and handled properly. However, exception handling can be overdone. There are scenarios where it is appropriate for your user to see a generic error screen (which of course sends you, the developer, an e-mail telling you an error occurred and hopefully what caused it). To see why, let's look at another saxophone
example:
try
{
MySaxophone.GetKeyByName("G").Press();
}
catch
{
}
Unfortunately, I've seen multiple applications where the exception handling code (in the "catch
" section) does nothing whatsoever - the application just moves along without giving the user any indication that there was a problem. The reasoning behind that approach seems to be that if the user doesn't see an error screen, there will be fewer complaints. While this may be true, the result is that you (as the programmer) won't get helpful information to solve your problem. The user won't know a serious error occurred. If they notice anything at all, it will be that the application doesn't work as they expect, so you may get complaints about your user-interface, faulty data, or maybe 100 other things, none of which point directly to your problem.
In this case, a saxophone without a G key wouldn't be of much use to most players, so hiding the error from the user isn't a good idea. If saxophones often didn't have G keys, I would suggest using local error-handling code to show the error to the user and tell them ways they might solve the problem themselves. Since saxophones shouldn't be missing G keys, you should strongly consider skipping the localized error-handling logic completely. Let your global routine handle this error, since having a saxophone without a G key is serious enough for the user to take notice and for you to be notified immediately.
Focus on the Big Picture
Ultimately, the goal is to allow the person maintaining your code to focus on the problem at hand, rather than sorting through your mess. The usual methods of making code maintainable, such as naming conventions, patterns, etc., are only useful if they give the next person a way to understand what you were trying to do. So don't feel like you have to pick the "right" naming conventions, patterns, etc., as long as you are continually making an effort to make your code readable. And remember, if another programmer doesn't understand your code, it's probably your fault.
Further Reading
If you wish to read further on the subject, I highly recommend Steve McConnell's book "Code Complete", available from Microsoft Press. If you ignore the fact that the title is grammatically awkward, it is a very good book, filled with practical advice every programmer should consider while writing code.