Structured Programming
The original goal of structured programming may have been to organize code by banning the "goto" statment. The problem with the goto probably revolved around the notion that the target of the goto statement could be anywhere in the code. If there are a million lines of code (and I've worked on project with a million lines of code) it could take a while to find the target. Editors and machines were not as sophisticated as they are now. In the middle 70s I worked briefly (quite briefly) on a system that used paper tape as the storage medium. Since before that I had worked on time-sharing systems with moderately better editors my time on that system was very brief.
We now have several better control sturctures although sometime a goto may be required to easily and cheaply exit a function with all the proper cleanup performed in one place. If that happens perhaps the function needs some additional scrutiny.
I don't particularly like "structured programming" as I believe that it makes the code unneccessarily complex. One entry, one exit is the particular rule that I dislike. I once was given the task of fixing a problem. When I looked at the code it appeared that the problem was in one function. The major issue in solving the problem was "One Entry, One Exit" as the function was 40 - 8.5" x 11" pages in length. An "if" statement might have a corresponding "else if" statment a few pages away. How does one understand such a function?
Abstraction
My approach is to try to find abstractions in the code. Then move the code that implements the abstraction to separate functions. This of course is risky, but if done carefully will succeed in reducing complexity sufficiently to solve problems.
Just moving the code to separate functions doesn't always simplify the code. Here is a made up example:
bool prepareFlight() { |
bool rslt; |
|
if (wingsWaggle()) { |
if (oilPressureNormal()) { |
if (brakesWorking()) { |
rslt = true; |
} |
else { |
tellMechanics("Brake Problem"); rslt = false; |
} |
} |
else { |
tellMechanics("Oil Pressure Problem"); rslt = false; |
} |
} |
else { |
tellGroundCrew("Flags Not Removed"); rslt = false; |
} |
return rslt; |
} |
Maybe a little over the top with this example but this is what I would do with the same function:
bool prepareFlight() { |
bool rslt; |
|
if (!wingsWaggle()) {tellGroundCrew("Flags Not Removed"); return false;} |
|
if (!oilPressureNormal()) {tellMechanics("Oil Pressure Problem"); return false;} |
|
if (!brakesWorking()) {tellMechanics("Brake Problem"); return false;} |
|
return true; |
} |
There are two advantages and one tricky part to this organization. The first is that the consequences of failure are immediately obvious to the reader (and can be ignored if that is not related to the problem at hand). No searching for that "else" clause. The second is that the function is actually smaller than the first version. The tricky part is the cleanup if the function requires it. All exits from the function must provide correct cleanup. Sometimes it is just calling one cleanup function before the return. Another way is to requre the caller to do the cleanup in both cases. Just don't forget to do it and don't hide it in a mass of code (i.e. make it obvious to the reader).
"break" and "continue"
Loop bodies may contain "break" and "continue" statements. These provide excellent ways to conditionally leave or resume a loop when that is the correct behavior. The continue statement is particularly important when there are several preconditions to performing some action of the target of a loop. Test for each precondition on the target and "continue" the loop when the precondition is false.
Likewise, in a loop often there some feature of the target of the loop that is being sought. Rather than deal with the target when found inside the body of the loop, leave the loop with a "break" and deal with the target after the loop. Note, one must be sure of success when leaving the loop but that is usually easy as more often than not a pointer to the target is used in the loop. When the loop fails to find the target the pointer should be zero (an easy test in C/C++).
Even better, make the search for the target a function of some kind that returns either zero or a pointer. Then inside the body of the loop a return statement is used to leave the loop and function. After the loop fails to find the target a return of zero will indicate failure.
Sometimes a function needs to be used in some sort of conditional statment (if, for, while). Then the function should be written as follows:
bool find(String name, Target*& tgt); |
Here the function use the reference to a pointer in its own loop. When the name is found in the target structure a value of true is returned. If the loop concludes without finding the name a value of false is returned.
When looking at all records in some data structure, returning a pointer to the first and next record are two useful functions. When finding a single record returning a boolean (bool) and a pointer to the record found is a useful function.