Dumb bugs
Volume | | 9
|
Number | | 11
|
Column Tags | | State of the Industry
|
Glenns Philosophy on Programming
Tricks to avoid wasting time on dumb bugs
By G. Weinreb, Somerville, Massachusetts
The most important thing in software development is that the programmer must love their code. If the programmer does not love his code, coding becomes a chore as opposed to a joy, causing code quality, coding productivity, and the programmer's mental state to degrade. In order for a programmer to love their code, ALL of the following must be true:
The code must be well organized with specific functionality localized to one area, as opposed to appearing multiple times.
ALL routines must be extremely well tested.
ALL routines must be extremely well documented.
Many routines must contain defensive code, especially the low level ones.
The programmer must have a decent machine with which to develop.
The programmer must have a decent development environment and a decent understanding of how to use it.
The programmer must have a good debugger. Working with complex black boxes, into which you cannot peer, is a hell that I would not wish upon even the fiercest of competitors.
The fundamental principle behind programming is that each issue must be localized to one area (e.g. one function); and that this one area do it's job extremely well (i.e. employ defensive code, include outstanding documentation, and be extremely well tested). And then all the other routines are to rely on ONE to implement the specific functionality. If similar functionality is copied and implemented in different places, then the programmer has less time to make each copy solid. Unsolid (non defensive, non documented, non tested) code leads to the following scenario:
A programmer is programming one function that calls another, and finds that the called one is not working well. So the programmer stops what he/she is doing and goes to fix the lower level routine. Now, if it is undocumented, the programmer needs to read each line, figure out what it's doing, and then document it - which can take more time than coding the thing from scratch. Therefore, undocumented code is in a sense disposable - you use it once and then throw it away and get another (like a Kleenex) when you need to change it. So the poor programmer documents the routine, tests it and makes it solid. But then we find that many other routines call this function, and our making it solid has changed it a little, causing it to not work for some of it's other callers. Subsequently, the programmer must fight to make it work in all cases - which requires coding in other areas - which can lead to new frontiers. And much time goes by while the poor programmer has accomplished very little on the original routine. And subsequently, they find it very hard to love their code.
So,
Code MUST be well documented, which includes:
+ function headers for all functions
+ descriptive headers for each file
+ descriptions of passed arguments
+ documentation in the code which describes what it does and how it does it
Code must be thoroughly tested because the devil is in the details and the details are what will bite you. Subsequently, you've got to DIG, DIG, DIG to uncover those subtle little problems. And the key to this is to have the confidence that they exist, else you will not dig for them - SO ASSUME THEY EXIST AND DIG, DIG, DIG.
Code MUST be defensive in order to get ahead of the bugs. Defensive coding entails looking for problems and showing an alert if one is found. Several examples:
a) Arguments passed to low level routines (i.e. the one's called by many other routines) are checked if doing development. A global variable, 'gDevelopment', is set TRUE when doing development, and at any time, the user can press an Option key to toggle the state of 'gDevelopment' TRUE/FALSE. 'gDevelopment' is typically used in the following way:
/* 1 */
void SetUserItem (
DialogPtr dialog, //ptr to dialog box
short itemNr, //item's DITL #
ProcPtr doDraw) //procedure pointer for
//user DITL item
{
if (gDevelopment) {// if doing develop...
if (IfPtrIsNullOrBadYell ((Ptr) dialog, 1)) return;
// test for null or bad ptr
if (IfDitlNrIsBadYell (itemNr, TRUE)) return; // test for
bad ditl #
if (IfPtrIsNullOrBadYell ((Ptr) doDraw, 1))
return;// test for null or bad ptr
}
body .....
body .....
}
Subsequently, if there is trouble, the programmer knows about the problem before it causes harm. And bugs can cause harm in a manner which is not obvious and/or a manner which does not lead the programmer to the bad code (e.g. a bug in area A destroys memory in area B, which crashes 100 mouse clicks latter). Defensive coding is like being at the window with a shotgun when the burglar climbs in - "Hey, what the hell do you think you're doing?". Programmers must get ahead of bugs, not behind, in order to love their code.
Someone playing devil's advocate might say, "Doesn't it take time to install defensive code and doesn't it slow your program?". It does take some time to implement, yet not much since it involves mindless cut/copy/paste of similar existing code fragments. The time for the processor to check for 'gDevelopment' TRUE is less than a microsecond (for perspective, HLock() costs 121µs on a Mac IIcx). Subsequently, adding "if (gDevelopment)" increases execution time by only a tiny amount. Freeing the programmer from fighting bugs will give him/her more time to optimize code - which will save orders of magnitude more execution time.
b) One can use macros to add defensive coding to toolbox routines. e.g.
/* 2 */
#define HLock_(h) \
\
if (gDevelopment) { \
if (CheckHandle((Handle) h, TRUE, TRUE)) \
HLock((Handle) h);\
} \
else if (h){ \
HLock((Handle) h);\
}
This simple little macro has saved my life on a number of occasions.
c) We will define the phrase "memory problem" as code which writes to memory where it should not, and possibly causes trouble in an unrelated area and/or in an intermittent way - both of which are infamous to programmers. In the case of a "memory problem", one must have a strategy for dealing with it (other than fumbling around and saying, "Gee's, this really IS a tough one."). One technique is to use a global, 'gShakeLikeHell', which is toggled TRUE/FALSE by pressing an Option key (it is always set to FALSE when you first launch the application), and a macro defined as follows:
/* 3 */
#define SHAKELIKEHELLIFSHAKING\
\
if (gShakeLikeHell) \
ShakeLikeHellifShaking();
ShakeLikeHellifShaking() compacts the heap, creates handles, disposes of handles, and tests existing data structures. If you have an intermittent or nebulous bug, the best response is to install the SHAKELIKEHELLIFSHAKING macro throughout your code under development, run the application, and press the Option key to turn it on. Chances are you will crash, or see the problem sooner (i.e. closer to the bad code). Ideally you would like to isolate the problem area between 2 SHAKELIKEHELLIFSHAKING macros (which is a mechanism which identifies if the problem does or does not originate from a specified range of code). After installing the macro, you can leave it in your code - it cost less than a microsecond. [Except for the time spent in the ShakeLikeHellifShaking routine. - Tech. Ed]. Installing this macro may seem silly, yet I can attest it has worked beautifully on many occasions - encouraging me to love my code.