In the conception stage, errors happen when the programmer doesn’t fully understand the problem to be solved. In the expression stage, errors are due to not following the rules of the programming language or not writing what you meant. In the transcription stage, errors are due to hand-eye coordination problems, spelling misconceptions, and so forth.
There are two main ways to avoid conception-stage errors. The first is to write out the design in a textual or graphical format. The second is to implement a prototype.
In this section, we aren’t discussing the design of the system as a whole, which includes classes, methods, interfaces, and so forth, but rather the design of individual procedures, functions, and methods. There are several good books on the topic of object-oriented analysis and design that cover the design of classes, methods, and interfaces. They don’t cover the design of individual methods, since the structured programming era handled this satisfactorily.
Start avoiding errors in your code by writing out the design in some design medium. Designs need to be reviewed. This seems obvious to many people, but there are reasons related to debugging that are important. Human memories are fallible, so you can’t review a design you haven’t written down. Your colleagues can’t read your mind, so a written design is essential to peer review. Writing down designs has the desirable side effect of delaying coding. As soon as you start coding, you start introducing bugs into your software.
After you code and execute your procedure, if testing reveals the existence of defects, you can compare the design with the procedure source to see if you really coded what you meant. All AI systems for finding bugs work by comparing a design and its realization. You need to be able to use the same technique to find bugs in your programs.
There are several notations suitable for writing out the design of a procedure. They all date back to the structured programming era. Some of them are as follows:
See the bibliography for recommended books on this topic.
In these design notations, data structures aren’t fully declared but are described in terms of high-level data structures, such as dynamic arrays, trees, sets, and so forth. Low-level structures used to implement these high-level structures, such as fixed-length arrays, linked lists, and hash tables, are not normally used. In addition, we recommend that only structured control constructs be used. For this reason, we haven’t included the venerable flowchart in our list of design notations.
Here is a small list of questions to use in reviewing the design of a procedure:
What assumptions are being made about the input and program state? Are they valid?
Are other logical possibilities not being handled?
Is there code to handle all default cases?
Is there any other action that must be taken at this point in the algorithm?
Is it always necessary to perform this action at this point in the algorithm?
Is this action the correct one to perform when this condition occurs?
Are these actions performed in the correct order?
An alternative to traditional design methods is to code and execute your algorithm in a VHLL. Such languages allow the programmer to omit details, often at the expense of range of expression and execution efficiency. APL, LISP, Scheme, and SETL are examples of languages useful for this work. They provide implied control structures and high-level data structures. Scripting languages such as Perl and Tcl can also be used for prototyping.
Rapid prototyping has all the advantages of a written design plus an additional one: You can execute it to find design flaws. This creates a higher level of confidence than does a reviewed design.
Two approaches are useful in this context:
Disciplined use of simplifying assumptions, developed by Rich and Waters [RW82]
The concept of scale models, explained by Weiser [We82a]
Assumptions must simplify the problem in a significant way without changing the essential character of the problem. The designer must keep track of the assumptions made so that they can later be retracted in the full-scale system.
Good simplifying assumptions have the qualities of consistency and continuity. A consistent assumption permits the prototype to work correctly for at least some real inputs. Ignoring invalid inputs is a consistent assumption. A continuous assumption makes it possible to modify the prototype so that it increasingly performs as a real system. Building a prototype of a parallel system without synchronization isn’t a continuous assumption.
A scale model is accurate in some ways but inaccurate in others. Different kinds of scale models have a different set of attributes that are scaled down. The model must interact with the real world in a limited, but natural, way.
A functional prototype does the critical system computations with the input domain scaled down. Special restricted formats for input are allowed, and only valid inputs need to be processed. The user interface is scaled down. The output format may only be adequate for manually validating the computations.
A user-interface prototype shows the user the following:
Options that will be available
Mechanisms to access those options
Messages generated by the system
Displays and reports that will be shown
Computation is scaled down, and output may be canned, rather than computed. Performance is scaled down as well. Tools that generate the dialog may be used, even though they create slower interfaces.
A performance prototype demonstrates whether a system can meet time or space limitations. It implements the features of the system that consume the most resources. The system features are scaled down so that computations known not to be resource intensive are omitted. User interfaces are scaled down, so that only those features that have response time requirements are implemented.
There are several ways to avoid expression-stage errors:
Exploit redundancy in your program.
Use tools that pick at programming nits.
We exploit redundancy differently in software development than we do in the design of tangible objects. With tangible objects, redundancy is largely used to provide a margin of safety that will cover unintended stresses on the object. It also covers for simplifications and assumptions made during the mathematical modeling. Thus, an engineer will specify additional material to be used beyond the minimum required for proper functioning to provide a safety factor. This type of redundancy masks failures in the original design.
With software, we include redundancy to expose failures as early as possible. Some of the redundant information we recommend will expose errors at compile time. Other information will uncover problems when the application is executed, hopefully during testing, rather than after the product is completed.
There are two other types of redundancy, which we don’t discuss further. These types of redundancy provide the same masking of failures as that used in the design of tangible objects.
Execution redundancy provides for executing the same program on multiple, identical platforms. This type of redundancy is found in fault-tolerant systems, used primarily in commercial environments where downtime results in severe financial losses.
Computational redundancy provides for executing different programs on multiple identical platforms. These different programs, developed independently, are supposed to compute the same result. When the results differ, a majority vote determines the correct result. The development and hardware resources required for this kind of redundancy are very expensive. This kind of redundancy is only used in situations where failure would be catastrophic, such as in space shuttles or nuclear reactors.
Providing data a compiler doesn’t need to generate a correct program enables it to identify some incorrect programs. In Fortran, both the type of a variable and its initial value have defaults, depending on the first letter of the name. If a name begins with the letters “I,” “J,” “K,” “L,” “M,” or “N,” then the type defaults to INTEGER; otherwise it defaults to REAL. In PL/I, all attributes have a default value. In C and C++, external and static variables are initialized by default to 0. Rather than use defaults, specify values explicitly.
Make full use of user-defined data types. Define numeric variables in terms of a unit of measure or the set of values they take on. Only programming artifacts, such as loop counters, are exempt from the possibility of typing in this way.
Permissive languages often have nitpicking tools to complement their compilers. These languages include Fortran, C, PL/I, and Scheme.
Strict languages require compilers to find the sort of problems that nitpicking tools find. These languages include Pascal, Ada, C++, and Java. There are such tools for these languages, but they look for a very small set of possible problems.
One way to perform nitpicking is to convert to a stricter variant of the language or just to use the compiler for a stricter variant. For example, you can convert from Fortran 77 to Fortran 95, or use a C++ compiler on a program that was originally written in C.
It is important to maximize the benefit and minimize the cost of using nitpicking tools. There are several ways to do this:
Keep a record of which messages turn up errors.
Use the tool’s control knobs to turn off messages that never uncover errors in your code.
Write a program to filter out the noise, if necessary.
Check your code regularly to make the task less burdensome.
Adopt a zero-tolerance policy for warnings.
See Chapter 14 for further details on nitpicking tools.
There are several ways to avoid transcription-stage errors:
Compensate for human psychology.
Exploit redundancy in your program.
There are at least three ways to compensate for human psychology to avoid transcription stage errors:
Overcome psychological set.
Maximize psychological distance.
Prevent typographical errors.
Psychological “set” reduces the ability of programmers to find errors [Uz66]. In our context, set causes the brain to see what it expects to see, rather than what the eye actually perceives. A transcription error may be completely masked by the set of the author. He or she knows what the program is supposed to say and is incapable of seeing what it actually says.
One way to overcome your psychological set is to have another person read your program. That person won’t come to the text with the same assumptions you have. If you’re working alone, leave the workplace and do something that has nothing to do with programming. When you return, your change of venue will often have broken your set.
A second way to overcome your psychological set is to display the program in a different format than that in which you’re accustomed to viewing it. Here is a list of different ways to look at a program listing:
Look at a paper listing instead of a video display.
Display one token per line.
Display a blank space after each token.
Display the listing with the comments deleted.
Display everything in reverse video.
Display different lexical categories in various formats. Lexical categories include keywords, numeric literals, string literals, user names, and those glyphs that aren’t numbers or letters. Different formats include all uppercase, italics, boldface, different colors for each category, and so forth.
Display numbers in equivalent formats. Show them in hexadecimal notation, scientific notation, or spelled out in words.
Display the program nesting structure in varying colors.
A third way to overcome your psychological set is to listen to the program being read. You can ask another programmer to read it to you. You can also have a voice synthesis program read it to you. In both cases, read along as the program is read aloud. You will notice that other human readers will separate and group items differently than you do.
All misreadings aren’t created equal. Information theory says that messages have a distance that is computed by counting the number of positions in which they differ [SW49].
The distance between “word” and “work” is one. The distance between “word” and “warm” is two. To reduce the likelihood of transcription errors, maximize the distance between characters and words that may be confused.
The number of different characters is only a starting point. Some pairs of letters and digits are much easier for the brain to misread than others. Easily confused pairs include the following:
0 and O
k and x
i and l
w and v
m and n
The positions of differing characters are also important. When words differ in the first or last positions, people are less likely to misread them. Consider how likely you might be to confuse the following pairs of words:
“word” versus “ward”
“word” versus “cord”
“word” versus “worm”
Errors in keying must be considered when selecting names for constructs in a program. Names that differ only in characters adjacent on the keyboard are more likely to be mistyped. Touch typists are more likely to mistype characters that are reached by the same fingers on opposite hands.
Choose names according to the following rules:
Prefer names that have a minimum distance of two characters from other names.
One of the differing characters should be at the beginning or end of the word.
Prefer names that aren’t likely to be misread as another name.
Avoid names that differ only in characters that have similar shapes.
Prefer names that aren’t likely to be mistyped as another name.
Avoid names that differ only in characters that are next to each other on the keyboard, or are on the same fingers of opposite hands.
Make programming constructs do double duty. It is a common error in C to mistype or confuse equality (two equals signs) with assignment (one equals sign). C allows assignments to be embedded anywhere in expressions. Some compilers will warn about every embedded assignment that feeds a conditional. While this can be helpful, embedding assignments in loop controls in C is common and efficient. Looking through hundreds of false alarm messages is discouraging and unproductive.
(var == 1) (var = 1) /* error missed */ (1 == var) (1 = var) /* error found */ C error unnoticed DO 10 I=1.100 C compiler sees DOl0I=1.10 C error detected DO 10 I=1.100,1