Flylib.com

Books Software

 
 
 

26.3 Design Principles

I l @ ve RuBoard

26.3 Design Principles

There are a couple of basic design principles you should keep in mind when creating your design. They will help you create a design that not only works, but is robust and elegant.

The first is "Think, then code." Far too many people, when given an assignment, can't wait to start coding. But the good programmers spend some time understanding the problem and studying all aspects of it before they start coding. After all, if you are driving from San Diego to Chicago, do you jump in the car and head north-east, hoping you'll get there, or do you get out a map and plan your route? It's a lot less trouble if you plan things before you start doing.

Following Orders

One of my favorite movies is called The King of Hearts . It is set in World War I. My favorite scene is where the general is giving three commandos their orders. This trio is a crack unit, well-known for instantly following orders.

"Men," begins the General, "I want you to leave right away."

The three turn around and run off.

"Stop" shouts the General. They halt and turn around. "Where do you think you're going?"

"No idea, sir," they answer in unison .

Far too many programmers act like these commandos. They run off and start typing on the keyboard before they have an idea where they are going.

The other design principle is "be lazy" (a.k.a efficient). The easiest code you'll ever have to implement and debug is the code that you designed out of existence. The less you do, the less that can go wrong. You'll also find that your programs are much simpler and more reliable.

Design Guideline: Think about a problem before you try to solve it.

Design Guideline: Be as efficient and economical as possible.

I l @ ve RuBoard
I l @ ve RuBoard

26.4 Coding

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, 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:

This procedure takes a number and returns its square.

A badly designed procedure tries to do multiple jobs. For example:

  • Depending on what values are passed in, this function will

    1. allocate a new block of memory on the heap,

    2. delete a block of memory from the heap, or

    3. change the size of a heap block.

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 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 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 variables

All 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
figs/c++2_2601.gif

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 hiding

A 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 details

There are some coding rules for procedures that have been developed over time; if used consistently, they make things easier and more reliable:

  1. For every C++ program file (e.g., the_code.cpp), there should be a corresponding header file (e.g., the_code.h ) containing the prototypes 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.

  2. The C++ program file and the header file should have the same name with different extensions, for example, the_code.cpp and the_code.h .

  3. 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 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
figs/c++2_2602.gif

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 Interconnections

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 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);

[1] 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 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?

  • 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.

  • The caller knows that the I/O package uses fixed size buffers. (The fact that MAX_BUFFER 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 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 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 cutter designed to cut out tennis shoes. The major components 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 buttons and indicators).

The basic design results in five major modules:

  1. 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).

  2. The positioning control system, which is responsible for moving the cutting head around and doing the actual cutting.

  3. A hardware-monitoring 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.)

  4. The control panel input module. This module is responsible for handling any buttons that the operator pushes.

  5. 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
figs/c++2_2603.gif

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.

Testing "Right"

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 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