< Free Open Study > |
Another key aspect of defensive programming is the use of debugging aids, which can be a powerful ally in quickly detecting errors. Don't Automatically Apply Production Constraints to the Development VersionA common programmer blind spot is the assumption that limitations of the production software apply to the development version. The production version has to run fast. The development version might be able to run slow. The production version has to be stingy with resources. The development version might be allowed to use resources extravagantly. The production version shouldn't expose dangerous operations to the user. The development version can have extra operations that you can use without a safety net. Further Reading For more on using debug code to support defensive programming, see Writing Solid Code (Maguire 1993). One program I worked on made extensive use of a quadruply linked list. The linked-list code was error prone, and the linked list tended to get corrupted. I added a menu option to check the integrity of the linked list. In debug mode, Microsoft Word contains code in the idle loop that checks the integrity of the Document object every few seconds. This helps to detect data corruption quickly, and it makes for easier error diagnosis.
Introduce Debugging Aids EarlyThe earlier you introduce debugging aids, the more they'll help. Typically, you won't go to the effort of writing a debugging aid until after you've been bitten by a problem several times. If you write the aid after the first time, however, or use one from a previous project, it will help throughout the project. Use Offensive ProgrammingExceptional cases should be handled in a way that makes them obvious during development and recoverable when production code is running. Michael Howard and David LeBlanc refer to this approach as "offensive programming" (Howard and LeBlanc 2003). Cross-Reference For more details on handling unanticipated cases, see "Tips for Using case Statements" in Section 15.2. Suppose you have a case statement that you expect to handle only five kinds of events. During development, the default case should be used to generate a warning that says "Hey! There's another case here! Fix the program!" During production, however, the default case should do something more graceful, like writing a message to an error-log file. Here are some ways you can program offensively:
Sometimes the best defense is a good offense. Fail hard during development so that you can fail softer during production. Plan to Remove Debugging AidsIf you're writing code for your own use, it might be fine to leave all the debugging code in the program. If you're writing code for commercial use, the performance penalty in size and speed can be prohibitive. Plan to avoid shuffling debugging code in and out of a program. Here are several ways to do that: Use version-control tools and build tools like ant and make Version-control tools can build different versions of a program from the same source files. In development mode, you can set the build tool to include all the debug code. In production mode, you can set it to exclude any debug code you don't want in the commercial version. Cross-Reference For details on version control, see Section 28.2, "Configuration Management." Use a built-in preprocessor If your programming environment has a preprocessor as C++ does, for example you can include or exclude debug code at the flick of a compiler switch. You can use the preprocessor directly or by writing a macro that works with preprocessor definitions. Here's an example of writing code using the preprocessor directly: C++ Example of Using the Preprocessor Directly to Control Debug Code#define DEBUG <-- 1 ... #if defined( DEBUG ) // debugging code ... #endif
This theme has several variations. Rather than just defining DEBUG, you can assign it a value and then test for the value rather than testing whether it's defined. That way you can differentiate between different levels of debug code. You might have some debug code that you want in your program all the time, so you surround that by a statement like #if DEBUG > 0. Other debug code might be for specific purposes only, so you can surround it by a statement like #if DEBUG == POINTER_ERROR. In other places, you might want to set debug levels, so you could have statements like #if DEBUG > LEVEL_A. If you don't like having #if defined()s spread throughout your code, you can write a preprocessor macro to accomplish the same task. Here's an example: C++ Example of Using the Preprocessor Macro to Control Debug Code#define DEBUG #if defined( DEBUG ) #define DebugCode( code_fragment ) { code_fragment } #else #define DebugCode( code_fragment ) #endif ... DebugCode( statement 1; <-- 1 statement 2; | ... | statement n; <-- 1 ); ...
As in the first example of using the preprocessor, this technique can be altered in a variety of ways that make it more sophisticated than completely including all debug code or completely excluding all of it. Write your own preprocessor If a language doesn't include a preprocessor, it's fairly easy to write one for including and excluding debug code. Establish a convention for designating debug code, and write your precompiler to follow that convention. For example, in Java you could write a precompiler to respond to the keywords //#BEGIN DEBUG and //#END DEBUG. Write a script to call the preprocessor, and then compile the processed code. You'll save time in the long run, and you won't mistakenly compile the unpreprocessed code. Cross-Reference For more information on preprocessors and for direction to sources of information on writing one of your own, see "Macro Preprocessors" in Section 30.3. Use debugging stubs In many instances, you can call a routine to do debugging checks. During development, the routine might perform several operations before control returns to the caller. For production code, you can replace the complicated routine with a stub routine that merely returns control immediately to the caller or that performs a couple of quick operations before returning control. This approach incurs only a small performance penalty, and it's a quicker solution than writing your own preprocessor. Keep both the development and production versions of the routines so that you can switch back and forth during future development and production. Cross-Reference For details on stubs, see "Building Scaf-folding to Test Individual Routines" in Section 22.5. You might start with a routine designed to check pointers that are passed to it: C++ Example of a Routine That Uses a Debugging Stub void DoSomething( SOME_TYPE *pointer; ... ) { // check parameters passed in CheckPointer( pointer ); <-- 1 ... }
During development, the CheckPointer() routine would perform full checking on the pointer. It would be slow but effective, and it could look like this: C++ Example of a Routine for Checking Pointers During Development void CheckPointer( void *pointer ) { <-- 1 // perform check 1--maybe check that it's not NULL // perform check 2--maybe check that its dogtag is legitimate // perform check 3--maybe check that what it points to isn't corrupted ... // perform check n--... }
When the code is ready for production, you might not want all the overhead associated with this pointer checking. You could swap out the preceding routine and swap in this routine: C++ Example of a Routine for Checking Pointers During Production void CheckPointer( void *pointer ) { <-- 1 // no code; just return to caller }
This is not an exhaustive survey of all the ways you can plan to remove debugging aids, but it should be enough to give you an idea for some things that will work in your environment. |
< Free Open Study > |