| I l@ve RuBoard |
|
26.4 CodingWe'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 DesignA procedure in C++ is like a paragraph in a book. It is used to express a single, coherent 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 sentence. For example:
A badly designed procedure tries 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. Design Guideline: Procedures should be no more than two or three pages long. 26.4.1.1 Procedure interfaceThe 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 tells 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. 26.4.1.2 Global variablesAll the variables used by a procedure are either local to the procedure or parameters except 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 anywhere 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. Design Guideline: Use global variables as little as possible. 26.4.1.3 Information hidingA well-designed procedure makes good use of a key principle of good design: information hiding. All the user 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 clients said, "Tell me what I have to know and shut up about the other stuff." 26.4.1.4 Coding detailsThere are some coding rules for procedures that have been developed over time; if used consistently, they make things easier and more reliable:
26.4.2 Modules and Structured ProgrammingA collection of closely related procedures in a single file is called a module. 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 tested 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. Design Guideline: Arrange modules into a organized structure whenever possible. 26.4.2.1 InterconnectionsAlthough 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 writes 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: store_char(io_ptr, ch); if (n_char(io_ptr) >= MAX_BUFFER)[1] flush_buffer(io_ptr);
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 MAX_BUFFER. 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?
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 fixed-size buffers are bad and wish to use dynamic buffers. We'll add a function call get_max_buffer to our module. But what about all the modules out there that have MAX_BUFFER 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 ourselves. One final note: a better design would encapsulate the io_ptr 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 OrganizationLet's see how a set of modules can be organized in real life. In this case we are dealing with a computer-controlled cutter designed to cut out tennis shoes. The major components of this device are:
The basic design results in five major modules:
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 well-defined 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 parts, 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 damaging the carriage. (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. 26.4.4 Module SummarySo 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 next few sections, we'll see how to include data in our design through the use of object-oriented design techniques. |
| I l@ve RuBoard |
|