Four Ways to Avoid Problems Caused by Global Data
December 10, 2008 Ted Holt
Junior J. Programmer modifies an RPG program. His testing works properly. The user’s test works properly. The user approves the change, the modified program is installed, and everything is copasetic until a few months later when the program gets stuck in a loop. What happened, and why is Junior not entirely to blame?
Here’s the code Junior added to the program:
C 1 DO 5 X 1 0 C EXCEPT PLINE C ENDDO
What could go wrong with such simple code?
It turns out that the subroutine that contains Junior’s code is also conditionally and indirectly executed from another part of the program.
C IF ACCOUNT <> *ZERO C 1 DO 8 X 1 0 ... calcs that indirectly execute Junior's code go here C ENDDO C ENDIF
Since the condition was not directly over the execution of his subroutine (i.e., there were intervening subroutine calls), Junior did not realize that his testing had not covered all conditions. Nor did the user test the conditional logic. The first time ACCOUNT proved not equal to zero, the program went into an infinite loop.
Junior’s mistake was in using the X variable to control the loop. His loop messed up the value of X in the outer loop. Since X was already defined as a one-digit packed variable, the compiler did not flag the duplicate declaration.
This example is only one of many ways that a modification to a program can cause an error in another, correctly working section of code. I call these unintended consequences undesirable side effects, and to my way of thinking, the best way to debug undesirable side effects is not to put them into the code in the first place.
In this case, the best way to avoid the infinite-loop problem would be through the use of a local variable, a variable that is known only within a section of code. Global data, on the other hand, is known throughout the entire program. Junior’s program went into a loop because X was a global variable.
Ideally, your program should have no global variables, but that is not practical. Here are four ways that you can minimize the use of global variables. You can use these techniques at any current release. (V6R1 presents more opportunities to localize data, but that’s a subject for another day.)
1. Use Subprocedures, Not Subroutines
Unlike subroutines, subprocedures can have local data (i.e., variables, constants, and data structures). Local data follows the parameter list. In Junior’s case, had the program been written to use subprocedures, he would have added variable X as a local variable to the subprocedure that he modified.
P OneSubProc b D pi D**** parms (... parms omitted ...) D*** local D X 1p 0 (... code omitted ...) C 1 DO 5 X C EXCEPT PLINE C ENDDO (... code omitted ...) P e
The subprocedure would have had a variable X, the calling routine would have had its own variable X, and the compiler would have kept them separate.
2. Minimize the Main Declaration and Calculation Specs
Ideally the D specs in the main body of an RPG IV program should be limited to parameter lists, compile-time tables and arrays, and program-wide data structures like the program status data structure. That is not always practical, but the closer you can adhere to this idea, the less chance you have of creating undesirable side effects.
The main calculations should contain a call to one or more subprocedures. Here’s an example from a previous article of mine.
H dftactgrp(*no) actgrp(Whatever) FXACTS o e disk usropn * ===== *ENTRY PLIST D SomePgm pr D inID 5p 0 D inName 12a D SomePgm pi D inID 5p 0 D inName 12a D/copy prototypes,assert D/copy qrpglesrc,psds D SomePgm_Main pr /free *inlr = *on; monitor; SomePgm_Main(); on-error; assert (*off: 'Unexpected error in program ' + %trim(psdsProcName) + '. See job log for details.'); endmon; return; // =========================== /end-free P SomePgm_Main b /free open xacts; ID = inID; Name = inName; write XactRec; close xacts; /end-free P e
The main program logic is in subprocedure SomePgm_Main, not in the main C specs. The only global data is in the parameter list and the files, but you can address that global data, too.
3. Give Each File a Unique Prefix
In releases before V6R1, all files must be declared globally, which means that all fields defined in files are global. Giving each file a unique prefix, and not using those prefixes on other data, decreases the chances of using a field or variable when it should not be accessed.
I recommend prefixes of one or two letters followed by an underscore.
FCustomer if e disk usropn prefix(c_) FMyReport o e printer usropn prefix(p_)
All fields from Customer begin with c_. All fields in the printer file begin with p_. If you don’t use these prefixes for anything else, there will be no confusion.
4. Use a Special Prefix for Global Data
I try to prefix global data with a small g. Any other convention is just as good. In the following example, the status parameter, from the program’s parameter list, and a tax rate variable are global data.
D QAD2242R pr D gStatus 1a D QAD2242R pr D gStatus 1a D gTaxRate s 9p 6
It goes without saying that this technique is effective only if you do not use a little “g” to begin local data names within subprocedures.
I don’t consider Junior completely at fault for introducing a bug into the program. While he should have used a better variable name than X, the original programmer could have written his source code with maintenance in mind.