Prevent program bugs right from the start and quickly exterminate existing ones!
Bug Proofing VISUAL BASIC(r)
Let's face it, most programs with more than ten lines of code contain bugs. And as you know, the longer a bug exists in a system, the harder it becomes to locate and repair. Help is here! Packed with code, this practical guide shows you how to write effective, error-free programs and, best of all, how to test your programs at crucial stages of development. You'll find out how to handle unexpected bugs that do occur and how to locate and fix them quickly. You'll get bug prevention and repair techniques that just aren't available anywhere else! You'll get the kind of advice and information that usually only comes from years and years of hard-won experience! You'll find out:
• How to design bug-free programs
• How to code proactively to prevent bugs before they start
• How to write code that exposes bugs instead of hiding them
• How to catch bugs before they do any serious harm
• How to find bugs using tools like the debugger and code profiler
• How to use debug and runtime versions of a program to make debugging easier
• How to use On Error statements to handle unexpected conditions
• How to record information automatically, so you can fix bugs after users encounter them
• How to use proven methods to find errors quickly
• How to create and analyze special foolproof tests for errors
|Product dimensions:||7.50(w) x 9.29(h) x 0.88(d)|
About the Author
ROD STEPHENS is a contract software engineer specializing in algorithm and graphical user interface design. A former senior member of the technical staff at GTE, Rod also conducted research and taught algorithms at Massachusetts Institute of Technology. He is a well-known columnist and the author of five bestselling Wiley books: Visual Basic Graphics Programming; Advanced Visual Basic Techniques; Custom Controls Library; Ready-to-Run Visual Basic Algorithms, Second Edition; and Ready-to-Run Delphi 3.0 Algorithms.
Read an Excerpt
Programming is a mental task, yet many programmers work with no mental discipline. In bug prevention, detection, and removal, mental attitude is crucial. If you assume that bugs are unavoidable, you will create bugs. If you assume the bugs can be fixed during final testing, you may find so many bugs at the end of the project that you can never finish. If you release your code before you test it, you are guaranteed someone else will find your bugs before you do.
This chapter explains how certain ways of thinking naturally lead to bugs while others prevent them. Some of these concepts may seem strange at first. After a while, they will become as straightforward as asking yourself, "Do I want a bug here, or not?"
Managers rarely write the majority of the code on a project, so it might seem odd to have a section about management. There are several reasons this section is here. First, even if you are not a manager, you may be a senior developer who provides guidance to others. Even if you are a junior programmer or the only person working on a project, you at least manage yourself. You may have little other control over your environment, but you should try to manage your own efforts properly.
Finally, if you are suffering from bad management, the following sections will help you understand what is happening and why. You may be able to improve your situation by discussing it with your boss or by leaving an anonymous copy of this book on his desk with certain key pages marked. At worst, you can decide for yourself whether bad management has destroyed any chance of success and it is time to update your resume.
You might think management would have little impact on the number and kinds of bugs in the code. Actually, both good and bad management can have a tremendous impact on developer productivity. By maintaining the right atmosphere, a good manager can help programmers produce more code with fewer bugs. By developing the wrong atmosphere, a bad manager can demoralize the team and make them produce less code containing more bugs.
The following sections explain attitudes you should have as a developer. They are also attitudes you should foster in others as a manager. If all of the team members share these attitudes, they can reduce the time wasted on bug chasing and spend more time writing code.
Fight Bugs Every Step of the Way
Think of bugs in a manic-depressive way. Be optimistic that all bugs can and will be found as quickly as possible. Be pessimistic about the chances of actually finding every last bug.
Bugs can and must be stopped before they enter working code. You cannot release code to other team members without thoroughly testing it first. The longer a bug remains undetected, the harder it is to fix. By testing new code immediately, you can stop bugs as soon as they are created and before they contaminate other developers' code. It must be your goal to never affect another programmer or user by releasing code that contains a bug. This goal is ambitious, but realistic.
On the other hand, you must realize that some bugs will slip into the project no matter how careful you are. This means you must ruthlessly hunt for bugs throughout the entire project, from the initial requirements analysis phase to the final testing and documentation phases.
Bugs are not a force of nature that must be tolerated. They are something you can predict, hunt, corner, and kill. A bug is not some natural phenomenon that spontaneously appears in finished software just in time for demonstrations to corporate vice presidents.
Catch Bugs Early
Application development is sometimes portrayed in pyramid form as shown in Figure 1.1. The design at the bottom provides a solid foundation for the coding and testing that follow. Actually, the process is usually much more complicated. Figure 1.1 omits many of the details, including requirements analysis, definition of objectives, system behavior specification, test objective analysis, and so forth.
In a pyramid made of stone, the broad base spreads the weight of the stone at the top. If the base contains a weak brick, the others share the load so the pyramid does not crumble.
Unfortunately, the situation is somewhat reversed in software development. If a bug enters the project during the design phase, it will influence later development. Even if later code is implemented perfectly, it may still be wrong because it relies on incorrect assumptions in the earlier stages. A flaw at the bottom of the pyramid can spread through the later stages of development, as illustrated in Figure 1.2, until the entire structure crumbles.
To prevent later disaster, you must catch bugs as quickly as possible. It is worth spending extra time during the early phases of the project to make sure bugs do not contaminate the whole effort.
Stay in Control
One of the most sweeping things you can do to protect your productivity is to create an atmosphere of control. If you feel pressured to meet deadlines, the quality of your code suffers. You will not properly design routines before coding them and you will not spend enough time testing routines afterward. This introduces bugs into the code. Later, you will find the bugs and, in trying to fix them hastily, make more mistakes.
Bugs breed bugs. One routine may compensate for the incorrect behavior of another. When the original bug is fixed, the compensating routine may break. With larger mistakes, you may write correct code based on faulty assumptions. When the assumptions are corrected, the code fails. Hastily repaired code becomes a fragile collection of dubious assumptions and exceptions instead of an elegant model of program behavior.
As you spend more time chasing and fixing these bugs that should never have occurred in the first place, morale sags. You soon become trapped in a vicious circle. You do not have time to do your job properly. You rush to finish new code and chase old bugs. Your haste creates more bugs that take even more of your dwindling time later. At this point, the bugs control you, not the other way around.
Breaking this cycle is not as difficult as you might think. All you need to do is avoid the bugs in the first place. That means performing adequate design before coding and adequate testing afterward. This reduces the number of bugs introduced during all stages of the project. You waste less time fixing avoidable bugs so you can spend more time on careful design, coding, and testing.
Pressure to meet deadlines and produce code more quickly can make you lose control again. Use these techniques to prevent backsliding:
Give design higher priority than deadlines. B>Plan carefully so you do not create bugs in the first place.
Give testing higher priority than deadlines. Take time to test your code as soon as it is written so you can catch bugs before they affect other programmers.
Use methodical care, not haste and carelessness. Do not make sloppy mistakes by working hastily.
Fix bugs as soon as you find them. Bugs only get harder to fix later.
Once you are in control, you can easily stay in control. You just need to force yourself to take the time needed to do your job properly. After some practice, good design and testing techniques become second nature, and staying in control is easy.
Intercept Unnecessary Work
If you manage other programmers, you can help them stay in control by intercepting unnecessary work. Do not make the team members waste their time on progress reports, executive summaries, and other administrative busy work. Take on as much of this as possible and leave the developers to their development tasks.
If you are not a manager, do not pass work on to other developers. If you are faced with a simple task that you can perform quickly and easily, do it. If you pass the job to someone else, you waste the time it takes you to tell them what to do and the time it takes them to understand what needs to be done.
Keep an eye on your time and make sure you are not doing unnecessary work. If you are losing a lot of time each day to tasks that do not help your project, try to eliminate some of your tasks. For example, in one project, I provided operating system and language support for the other project members. Occasionally, people outside the group would ask questions and I was happy to help. Over time, however, the outside demands grew. Eventually, I could barely keep up with my own tasks because I was spending so much time helping others. At that point I decided to answer outside questions only after 4:00 P. M. The outsiders stopped asking their simpler questions and I was able to regain control of my own work.
Sharing resources, even among different project teams, is a good thing. In this case, however, too much of a good thing could have jeopardized the project.
Programming is full of tradeoffs. One of the most common tradeoffs is speed versus size. Many programs can be made faster by using more memory. For example, suppose you have a function that calculates the shortest distance between two points in a street network. You could make the program much faster if you precompute the shortest distance between every pair of points in the network and then store the results in a table. Now instead of using the function whenever you need a value, you can look the value up in the table. This makes the program faster, but takes a lot of memory. If the street network connects 10,000 points, the table will contain 100 million entries.
There are several other tradeoffs you make while writing code. For instance, you implicitly decide
- How fast the code is
- How much memory the code uses
- How understandable the code is
- How likely the code is to contain bugs
- How much the code looks for errors
- How hard the code is to test
- How robust the code is
- How soon the code will be finished
You should discuss these tradeoffs with other project members so everyone has the same priorities. If one programmer places speed above all else and another places robustness first, they may be working against each other. Each may make unnecessary changes to code to try to meet these conflicting goals. Agree on the team's priorities so everyone has the same objectives.
Different stages in product development address different needs. The purpose of the initial development phase is to produce working code, and priorities should reflect that goal. You should place great emphasis on producing code that is understandable, maintainable, and less likely to contain bugs.
During the final stages of application development, you need to optimize the code. The goal is to improve the performance of those parts of the application that need it the most. Because the goals have changed, your priorities should change to match. At this point, it may be more important to produce fast code than code that is easy to read.
This does not mean you should start giving variables incomprehensible names, drop indentation that shows program structure, and stop using comments. It simply means you can use more complicated algorithms that you might have avoided earlier out of fear of complexity.
As the application matures, realign priorities accordingly.
Many programmers concentrate only on speed and memory usage. Unfortunately, these are usually far less important than readability, maintainability, and robustness, at least during the early stages of the project.
Typically, 90 percent of a program's time is spent in 10 percent of the code. Similarly, most of a program's memory use is often localized in a small number of routines and data structures. If you go to great lengths to optimize speed and memory use in every part of the program, 90 percent of your work is spent on code that does not need the extra effort.
Even worse, optimizing every part of the program makes much of the code more complicated, less understandable, and more likely to contain bugs. Not only does 90 percent of your work fail to make the application significantly faster or smaller, but it also makes matters worse by generating more bugs.
Initially, you should concentrate on making code bug free, understandable, testable, and maintainable. Later, when the project is almost finished, you can run tests to determine which code needs optimization. After you find the 10 percent of the code that takes 90 percent of the time, you can concentrate on improving only that code. You do not waste your time optimizing the other 90 percent of the code and you avoid all the bugs that premature optimization would have caused.
Deferring optimization also means that bugs created during optimization are introduced late in the project. The longer a bug remains hidden before it is detected, the harder it is to find and fix. Because the optimization bugs are introduced late in the project, you cannot have written a lot of code that depends on these bugs. That makes the bugs easier to find and repair.
For many years after their invention, computers were extremely expensive. A computer that was powerful enough to be useful cost millions of dollars. In comparison, programmers were cheap.
Today, computers are relatively cheap and programmers are very expensive. If a program is not fast enough, installing new memory or buying a faster computer are reasonable options. Unless the program will be used on hundreds of computers, it will be cheaper to buy more hardware than to make a programmer spend more than a few hours optimizing the program's code.
If an application's performance is adequate, do not optimize it. You will save not only the unnecessary optimization time, but also the time to fix the bugs optimization causes.
Get It Working First
High-quality code makes scheduling much easier. Suppose you have a project in which 90 percent of the code is written but riddled with bugs. Guessing when the last bug will be fixed is impossible. You cannot predict when the code will be working, which features will be working first, and when the finished application will be ready for release.
Contrast that with a project in which 50 percent of the code is written but it is very high quality and has almost no bugs. In that case you can predict with some confidence that the coding phase is about halfway finished. Since the team writes high-quality code, you know from your schedule which features will be working first. You may even be able to rearrange the schedule somewhat to change the order in which items are finished. After you include enough time for adequate testing, you can predict when the finished application will be ready for release.
A partial implementation that works is also better than a complete implementation that is full of bugs. Suppose one application is nearly complete but it crashes every five minutes. Another application is only half-functional, but that half is flawless. Menu items and buttons for features that are not yet implemented are disabled. People will perceive the second as the better, more professional application. If you doubt that, think about which you would rather demonstrate to your company's president.
Get the application working properly first. Then optimize it. Make sure each piece is working before you move on to the next.
Be Steady and Thorough
A programming adage states, "There's never time to do it right, but there's always time to do it again." If you rush through development to meet deadlines, you may do shoddy work. You make mistakes you would have avoided had you spent enough time to properly design, code, and test the program. Instead of finishing the project a little late, you may finish on time with a program so full of bugs it is worthless. You then need to rework the code again and again until you fix enough bugs so that the program is usable.
Be steady and thorough, not fast and careless. In programming, even more than in many other occupations, haste makes waste. Build a routine, test it thoroughly, and move on to something else. Do not rush frantically from task to task, trying to get as much done as possible as quickly as possible. If you take the time to ensure that your routines are bug-free when you write them, you will not need to debug them later.
On the other hand, unlike the tortoise in the fable "The Tortoise and the Hare," you do not need to be slow and steady. It is fine to be fast, as long as you are thorough. Design a routine, implement it, test it thoroughly, and then move on to the next task.
For many years, programmers have been taught to code defensively. Defensive coding means writing a routine so it can handle a wide variety of possible strange or incorrect inputs without crashing. For instance, consider the following implementation of the factorial function:
Public Function Factorial(num As Integer)
If num = 0 Then
Factorial = 1
Factorial = num * Factorial(num - 1)
This routine enters an infinite recursion if its parameter num is less than 0. If the parameter is -1, the program calls Factorial(-1), which calls Factorial(-2), which calls Factorial(-3), and so forth until the program exhausts the system stack and crashes.
The traditional defensive version of this routine uses an inequality test to catch cases where the parameter is less than 0.
Public Function Factorial(num As Integer)
If num <= 0 Then
Factorial = 1
Factorial = num * Factorial(num - 1)
This defensive version allows the Factorial function to continue running when it receives an invalid input. Unfortunately, it also silently hides a probable bug. Factorials are not defined for numbers less than 0. If the calling routine executes Factorial(-1), it probably contains a bug. The defensive version of the Factorial function hides the bug by continuing to execute as if the parameter were valid.
Instead of hiding the bug, the function should draw attention to it. It should explicitly check its arguments to verify that they make sense. If not, the routine should make the bug obvious by stopping or by raising an exception.
For example, in Visual Basic 5 and 6, the program can use the Debug.Assert statement to verify that the argument is at least 0. Debug.Assert takes a Boolean expression as an argument. If the expression is false, Visual Basic halts execution at the Assert statement. The developer can then examine the code to find the problem.
When it compiles the application, Visual Basic removes the Debug.Assert statement from the code. If the compiled program encounters this bug, it cannot stop because the Debug.Assert statement is missing. To allow the program to continue if an unexpected error occurs and the Debug.Assert statement has been removed by the compiler, the code can use defensive programming techniques.
Public Function Factorial(num As Integer)
' Validate parameters.
Debug.Assert num >= 0
If num <= 0 Then
Factorial = 1
Factorial = num * Factorial(num - 1)
Take the offensive and catch bugs before they catch you. Chapter 5, "Exposing Bugs," has a lot more to say about writing code that makes bugs obvious instead of hiding them.
Write for Others
Program as if someone else is going to read your code later; someone almost certainly will. Even when you read the code later, it may be far enough in the future that you may as well be someone else. By the time you read the code again, you may not remember any of the routine's finer details.
Assume the person reading your code is not the brightest programmer in the world and will misunderstand the code if possible. In many companies, maintenance programmers are often the least experienced so this may be close to the truth.
On the other hand, this person is a programmer and has at least some programming knowledge, so you do not need to explain every little detail about how Visual Basic works. You need to explain what the code is trying to do, not what the ReDim statement does.
The person may be trying to understand your code, or he may be trying to find a bug. The bug may be in your routine, or it may be in another routine that calls yours. To make it easy for this person to understand what your code is doing, keep the code as straightforward as possible. Remember that this later person may be you-the time you save may be your own.
Write for Humans
Computers execute programs but you need to write code for people. The computer does not care whether the code contains comments, proper indentation, and variables with meaningful names. It does not care if the code is written in Visual Basic, Delphi, or machine code. In fact, it does not care if the code does what it is supposed to do. The computer will not locate and fix bugs. It will not even notice a bug unless the program tries to do something illegal like divide by 0.
Only a programmer can examine the code and decide whether it is operating correctly. Only a human can find and correct a bug. Always remember that you write code for other people, not for the computer.
Code for the Ages
Do not use quick and sloppy techniques, expecting to clean the code up later. Chances are good that no one will have time. Assume your code will remain in use exactly as you write it for the next 30 years. Later, if you do have the free time, you can consider rewriting it. Of course, that may introduce new bugs. It is better to get the code right the first time and not rewrite it.
Do not expect to fix code in a future release. That may happen and it may not. There are probably trillions of lines of code still in use that were written 10, 20, or even 30 years ago. The programmers who wrote that code probably thought it would be rewritten a few years later. Had they been correct, there would never have been a year 2000 problem.
I once worked with a huge mainframe application that included code written over a span of more than 20 years. I am certain the programmers who started the project had no idea it would still be running decades after they finished the initial release. Over the years, the program had become a conglomeration of code written in several different programming languages. It included data that was compiled into the code. This is the equivalent of putting assignment statements directly into a Visual Basic program instead of loading the data from a database. Whenever the business rules changed, the program had to be recompiled. The company grew to depend on the program until it was absolutely essential to operations. The program cost more than $100 million per year to run, but it could not be rewritten because, after decades of uncoordinated changes, the code had grown too incoherent for anyone to understand.
Write as if your code will be used for many years to come. Chances are good it will be.
Code without Ego
Many programmers become emotionally attached to their code. That is only natural. If you spend a lot of time and effort building something, it is natural that you will become attached to it.
Unfortunately, code sometimes needs to be rewritten. It may need to be rebuilt to implement a better algorithm. After too many bugs have been found in one routine, it may be better to rewrite it from scratch rather than patch it further. Sometimes it may be completely thrown away and replaced with a different method.
You must be ready to let go of your code so it can be rewritten, replaced, or discarded.
Do not feel your code is a reflection of yourself. A bug in your code does not mean you are any less of a person. If you feel that a bug is a stain on your reputation, you will not look as hard as you should for bugs because you will not really want to find any. You will also not be receptive to others testing your code and reporting or fixing bugs.
Many programmers learn these lessons only after years of experience. Some developers never learn. They remain hostile to changes in their code no matter how obvious it is that the changes are improvements.
Remember that the goal is to build the best solution possible, not to stick with the original code until it is an incoherent mass of bug fixes and patches. Do not let ego and attachment stand in the way of improving the code.
Many of the ideas presented in this chapter cannot be easily demonstrated by example code. Pointing to a piece of code and saying it is the result of hasty work or too much emphasis on speed is difficult.
Program Bad1 violates several of the more easily demonstrated guidelines presented in this chapter. The program, shown in Figure 1.3, generates a random list of numbers. When you enter a value in the Target field and click the Search button, the program uses a binary search to locate the item you entered in the list.
The following code shows how program Bad1 works. Assume that this code has just been written near the beginning of the project, and that it has been accepted into the project's code library. You should be able to make several improvements. Appendix A, "Self-Test Solutions," contains a revised version of this code with some suggested changes.
Private Values(1 To 50) As Integer
Private Sub Form_Load()
Private Sub Ready()
Dim i As Integer
Dim txt As String
Values(1) = Int(Rnd * 10 + 1)
For i = 2 To 50
Values(i) = Values(i - 1) + Int(10 * Rnd + 1)
For i = 1 To 50
txt = txt & Format$(i, "@@") & _
Format$(Values(i), "@@@@") & vbCrLf
txtValues.Text = txt
Private Sub CmdSearch_Click()
Dim i As Integer
i = BinarySearch(Values, CInt(txtTarget.Text))
lblPosition.Caption = Format$(i, "@@") & _
Format$(Values(i), "@@@@") & vbCrLf
Public Function BinarySearch(list() As Integer, _
target As Integer) As Integer
Dim a As Integer
Dim b As Integer
Dim c As Integer
a = 1
c = 50
Do While a <= c
b = (c + a) / 2
If target = list(b) Then
BinarySearch = b
ElseIf target < list(b) Then
c = b - 1
a = b + 1
BinarySearch = 0
Mental discipline is critical for effective programming. The following Bug Stoppers summarize concepts that are helpful for maintaining a bug-free attitude. Attitude is everything. Without the proper mindset, you may write thousands of lines of code very quickly, only to waste months of extra time debugging them. By coding more carefully, you can produce more working code in less time. You can control the code and not let deadlines and bugs control you.
BUG STOPPERS: Programming Philosophy
Manage properly, even if you are the only developer.
Fight bugs every step of the way.
Catch bugs early.
Stay in control.
Intercept unnecessary work.
Set consistent priorities.
Change priorities as the project matures.
Don't optimize unless necessary.
Get it working first, then optimize if necessary.
Be steady and thorough, not fast and sloppy.
Code offensively to expose bugs, not hide them.
Write for others, not for yourself.
Write for people, not for the computer. Code for the ages.
Code without attachment or ego.
Table of Contents
Constants and Enums.
Error Handling Fundamentals.
Standard Error Handlers.