We're going to start our discussion at the bottom and work our way up. The smallest unit of code that we design is the procedure. Procedures are then used to build up more complex units, such as modules and objects. By starting simple and making sure our foundation is good, we can easily add on to create more complex, yet robust programs.
26.4.1 Procedure Design
A procedure in C++ is like a paragraph in a book. It is used to express a single,
thought. Just as a paragraph deals with a single subject, a procedure should perform a single operation. Ideally you should be able to express what a procedure does in a single simple
. For example:
This procedure takes a number and returns its square.
to do multiple jobs. For example:
It's the body of the procedure that does the actual work. A procedure should do its job simply and coherently. In general, programmers design and work on an entire procedure at a time, so the procedure should be small enough that the whole procedure can fit in a programmer's brain at one time. In practice this means that a procedure should be only one or two pages long, three at the most.
Procedures should be no more than two or three pages long.
22.214.171.124 Procedure interface
The public part of a procedure is its prototype. The prototype defines all the information needed by the compiler to generate code that calls the procedure. With proper commenting and documentation, the prototype also
the programmer using the procedure everything he needs to know. In other words, the prototype defines everything that goes into and out of the procedure.
All the variables used by a procedure are either local to the procedure or parameters
for global variables. (The word "except" is an extremely nasty word. Frequently it indicates a complication or extra rule. Thing were probably simple before the "except" came into the picture.)
The use of a single global variable inside a procedure makes the whole procedure much more complex. For example, suppose you want to know what a procedure does for a given call. If that procedure uses no global variables, all you have to do is look at the parameters to that procedure to figure out what is going to happen.
You can determine what the parameters are by looking at a single line in the caller. All the other variables are local to the procedure. That's only three pages long, so you probably can figure out what happens to them.
But now let's throw in a global variable. That means that the input to the procedure is not only the parameters, but the global variable. So who sets it? Because the variable is global, it can be set from just about
in your program. Thus, to determine the input to a procedure, you must analyze not only the caller, but also all the code in the entire program. I've seen people do string searches through tens of thousands of files trying to find out who's setting a global variable.
Figure 26-1 shows the information flow into and out of a procedure and how this is affected by global variables
Figure 26-1. Procedure inputs and outputs
One way people try to get around this problem is to require that all programmers list the global variables used by their procedures in the heading comments to the function. There are a couple of problems with this. First of all, 99.9% of the programmers don't do it and the other 0.1% don't keep the list up to date, so it's totally useless. In addition, knowing that a procedure uses a global variable doesn't solve the problems caused by not knowing when and how it is used by the outside code.
Use global variables as little as possible.
126.96.36.199 Information hiding
A well-designed procedure makes good use of a key principle of good design: information hiding. All the
of a procedure should see is the prototype for the procedure and some documentation explaining what it does. The rest is hidden from him. He doesn't need to know the details of how the procedure does its job. All he needs to know is what the procedure does and how to call it. The rest is irrelevant detail, and hiding irrelevant details is the key to proper information hiding.
Or as one of my
said, "Tell me what I have to know and shut up about the other stuff."
188.8.131.52 Coding details
There are some coding rules for procedures that have been developed over time; if used consistently, they make things easier and more reliable:
For every C++ program file (e.g.,
there should be a corresponding header file (e.g.,
) containing the
for all the public procedures in the C++ file. This header file should contain only the procedures for the corresponding C++ file. Don't put functions from multiple program files in a single header file.
The C++ program file and the header file should have the same
with different extensions, for example,
The C++ program file should include its own header file. This lets the C++ compiler check to make sure that the function prototype is consistent with the function implementation .
26.4.2 Modules and Structured Programming
A collection of closely
procedures in a single file is called a
. Modules are put together to form a program. The proper organization of modules is a key aspect of program design.
First, your module organization should be as simple as possible. Figure 26-1A shows a program with seven modules. With no organization, there are 42 connections between the modules.
Figure 26-2. Module interactions
A programmer who is debugging a module must make sure that the other six modules he deals with work. Any problems in them are her problem. Testing such a system is a problem as well. To test one module, you need to bring in the other six. Unit testing of a single module is not possible.
Now consider the organization in Figure 26-1B. This system uses a hierarchical module organization. Consider the benefits of this organization. The modules at the bottom level call no one, so they can be
in isolation. After these modules pass their unit tests, they can be used by the other modules.
People working on the middle-level modules have to contend with only two sub-modules to make sure their module works. They have some assurance their modules work ”after all, they did pass the unit test ”so the middle-level programmers can concentrate on dealing with their own modules.
The same thing holds true for the person dealing with the top-level modules.
By organizing things into a hierarchical structure, we've added order to the program and limited problems.
Arrange modules into a organized structure whenever possible.
Although Figure 26-2 indicates that one module calls another, it doesn't show the number of calls that are being made. If we've done a good job hiding information, that number is minimal.
Let's first take a look at an example of what not do to. We have a module that
data to a file. Some of the procedures are:
store_char -- Stores a character in the buffer
n_char -- Returns the number of characters in the buffer
flush_buffer -- Writes the buffer out to disk
When we want to write a character to the file, all we have to do is put the character in the buffer, check to see if the buffer is full, and, if it is, flush it to disk. The code looks something like this:
if (n_char(io_ptr) >= MAX_BUFFER)
The greater than or equal comparison (>=) is used instead of equal (==). as a bit of defensive programming. If somehow we overflow the buffer (
n_char(io_ptr) > MAX_BUFFER
), we'll flush the buffer and the program will continue safely.
This is an extremely bad design for a number of reasons. First, to write a single character to a file, the calling function must interact with the I/O module four times. Four? There are only three procedure calls. The fourth interaction is the constant
. So we have four connections where one would do.
One of the biggest problems with the code is the poor effort at information hiding. For this program, what does the caller need to know to use the I/O package?
The caller must know that the I/O module is buffered.
The caller must know the sequence of functions to call to send out a single character.
that the I/O package uses fixed size buffers. (The fact that
is a constant tells us that.)
All of this is information the caller should not need to know. Let's look at an alternative interface:
write_char(io_ptr, ch) -- Sends a character to a file.
This function may buffer the character, but it may not. All the caller needs to know is that it works. How it works is irrelevant. In other words, the system may be buffered, unbuffered, or use a hardware assist. We don't know and we don't care. The character gets to the file. That's all we care about.
Back to our original three-function call interface. Let's see what problems can occur with it. First, the caller must call the proper functions in the proper sequence each time. This is a needless duplication of code.
There is also a maintainability problem. Suppose we decided that
buffers are bad and wish to use dynamic buffers. We'll add a function call
to our module. But what about all the modules out there that have
hard-coded in them? Those will have to be changed. Because we have used poor information-hiding techniques, we have created a maintenance nightmare for
One final note: a better design would encapsulate the
data structure and all the functions that manipulate it in a single C++ class, as we will see later on in this chapter.
26.4.3 Real-Life Module Organization
Let's see how a set of modules can be organized in real life. In this case we are dealing with a computer-controlled
designed to cut out tennis shoes. The major
of this device are:
A computer that controls the machine. This computer is also used to store the patterns for the various shoes.
A positioning device for the cutting head.
An operator control panel (lots of
The basic design results in five major modules:
The workflow module. This module is responsible for scheduling the various cutting jobs that come up (e.g., do ten batches for size 9, then twelve of size 11, and so on).
The positioning control system, which is responsible for moving the cutting head around and doing the actual cutting.
system. Its job is to check all the status indicators and make sure that all the equipment is functioning correctly. (There's a lot of little stuff, such as oil filters, blowers, air filters, air supply, and so on, that all needs to work or we can't cut.)
The control panel input module. This module is responsible for handling any buttons that the operator pushes.
The control panel output module. All the blinking lights are run from this module.
A diagram of the major pieces can be seen in Figure 26-3.
Figure 26-3. Cutting system, module design
This organization, although not quite hierarchical, is quite simple. Each module has a
job to do. The modules provide a small, simple interface to the other modules.
The other thing about the modules is that they are designed to be independently tested. For example, when the project started, the machine didn't exist. What we had was a computer, a pile of
, and a lot of stuff on back order. Since the workflow manager didn't require any hardware, it was developed first. The other modules were faked with test routines. The fake routines used the same interface (header files) as the real ones. They just didn't do any real work.
It is interesting to note that the unit tests were used to test not only the software but the hardware. The unit test for the positioner module consisted of a front-end that sent various goto commands to the system. The first few tests were a little hairy because the limit switches had not been installed on the hardware, and there was nothing to prevent us from running the cutting head past the end and
. (Actually, the limits were rigorously enforced by a nervous mechanical engineer who held his hand inches above the emergency power off for the entire test. He wasn't about to let our software damage his hard work.)
This module structure let us create something that was not only simple, but testable. The result is increased reliability and decreased integration and maintenance costs.
The Camsco Waterjet cutter was the first machine ever designed and built to cut out tennis-shoe insoles with a high-pressure jet of water. Because it was a cutting-edge design, a lot of tuning and adjustment was required. In fact, the system was run for about a year before it was shipped to the customer.
During that testing phase, we had an agreement with the shoemaker: they would supply us with free material for testing if we gave them the cut parts.
To get proper performance data, we always used the same size in almost all of our tests, 9-right. After each test we boxed up the cut parts and shipped them to the shoemaker so they could make shoes out of them. Or so we thought.
Just as we were boxing up the machine for shipment, we got a call from the plant. "Are you the people who keep sending the 9-rights to us each month?"
We confirmed that we were.
"Well, I'm glad I finally tracked you down. Purchasing had no idea who you people are, so you were a little hard to find. Do you realize that you've shipped us ten thousand 9-rights and no lefts?"
Evidently the first production run for our machine was ten thousand 9-lefts.
26.4.4 Module Summary
So far we've learned a lot about how to design and organize our code. But programming deals with data as well as code. In the
few sections, we'll see how to include data in our design through the use of object-oriented design techniques.