If you're looking for information about using APIs to execute bound procedures, you may as well stop reading right now. This is about transparently executing alternate versions of bound procedures.
I recently needed to figure out how to do three things with ILE RPG:
- Configure an application with 100 or more parameters whose values vary by U.S. state
- Write a standard output document containing nonstandard sections that can be wildly different for each U.S. state
- Make such an application easy to maintain and understand
Configuring an Application with 100 or More Parameters
The first requirement isn't much of a challenge. It seemed obvious from the start that a configuration parameter file was the appropriate solution here. I designed a file that could hold default values and overrides by state as needed.
Configuration values are named in a way that would seem familiar to anyone who's ever worked with a UNIX resource file or the Windows registry. Each configuration value corresponds to a similarly named global variable in the application. A service routine loads and maintains the values. The application just uses them as if they are dust bunnies that appear effortlessly under the bed.
Writing a Standard Document Containing Nonstandard Sections
The second requirement is a little tougher. Configuration switches and option values can do quite a lot, but there are practical limits. A number of the requirements just couldn't always be satisfied with a standard routine. Historically, this problem is often resolved in the AS400/iSeries/i5/whatever world by using case selects, possibly in conjunction with subroutines or procedure calls based on the value of some internal variable, such as a state identifier. This particular application was already way past the level of complexity that can be intuitively understood by any human that I know. I didn't think I could afford the additional complexity of case selects and conditional subroutine execution. So how does one get from here to there without invoking a wormhole to Alpha Centauri? That's the subject of this article, but let's put it aside for a minute and talk about requirement three.
Making It Easy to Understand
You might be surprised at the amount of requirement detail you can absorb and intuitively use while writing a program. I think it's described as f(x) = Lim(Σф)x/Ю, as the number of telephone calls (Ю) approaches infinity. Anyway, there is a limit, looming like the door to tomorrow. No information returns from beyond it. There are probably sea monsters. I needed to get past it.
I decided to group related operations together in a way that would allow me to think of them as pseudo-operations, or black boxes. Instead of fetching the row from the database, setting the base color to red if the border matrix is less than .0379, and attaching the pedestal offset, I would simply GetFramis(). No other code in the program needs to know what's in a framis. In fact, the whole concept breaks down if any other routine is allowed to mess with the internal composition of the framis.
Once I've finished the first level of conceptual groupings, I can repeat the exercise and execute GetFramis(), AssembleThrom(), and PackageAssembly() just by calling the BuildProduct() function. This is minimal encapsulation and data hiding. The concept has been used successfully for years in some circles. As a strategy, it has its own problems. For instance, BuildProduct(), while possibly a useful grouping of functions, may not truly be a concept. If that's the case, using it increases the complexity of the program, rather than decreasing it. Instead of understanding BuildProduct(), the developer needs to understand that BuildProduct() really is the container for the GetFramis(), AssembleThrom(), and PackageAssembly() functions. That's not helpful. It would be a bad thing.
Having accomplished real consolidation of function into coherent, conceptually knowable black boxes, the total program now becomes knowable at different levels of granularity and scope. It's still unknowable overall, but the gross function is understandable by examining the execution of the highest level of black box concepts (functions), and succeedingly greater levels of detail are knowable as long as the scope of that knowledge is limited to the function containing the detail.
I hope I didn't short-circuit on that last paragraph.
Back to the Real Problem
I now have a situation where the following code might be executed:
NewNozzle = GetNozzle(Nozzle.Spread);
EndIf;
That's fine most of the time, but some states place arbitrary restrictions on nozzle bandwidth, some require redundancy, some prohibit redundancy but require a disclaimer horn if bandwidth is less than 10 microns, and some prohibit distribution to felons but require notification of transshipment. A single GetNozzle() function that will handle all requirements for all states just isn't practical.
In the configuration table, I added a parameter called Get_Nozzle. I made the default value StdGetNozzle. If required, the value can be overridden for a specific state so that state AK might specify AkGetNozzle, state CA could specify CaGetNozzle, etc.
The startup utility functions, which implement the configuration for a state, load a variable pGetNozzle with the configured value so that when State = AK, pGetNozzle contains the value AkGetNozzle.
The startup module contains the following prototypes:
D aSpread Like(T_SPREAD) Const
D AkGetNozzle PR Like(T_NOZZLE)
D aSpread Like(T_SPREAD) Const
D CaGetNozzle PR Like(T_NOZZLE)
D aSpread Like(T_SPREAD) Const
It also contains the following variable definition:
The Export keyword means that this variable will be available to any module in the program that chooses to import it. The @GetNozzle pointer is set with a call to the GetFnPtr function:
The GetFnPtr function returns the pointer:
D GetFnPtr PI 16* PROCPTR
D ProcName Like(T_PROCNAME) Const
D RtnVal S 16* PROCPTR
/FREE
If ProcName <> '*NULL';
Select;
When ProcName = 'StdGetNozzle';
RtnVal = %Paddr(StdGetNozzle);
When ProcName = 'AkGetNozzle';
RtnVal = %Paddr(AkGetNozzle);
When ProcName = 'CaGetNozzle';
RtnVal = %Paddr(CaGetNozzle);
// Unrecognized function name
Other;
RtnVal = *NULL;
EndSl;
Else;
RtnVal = *NULL;
EndIf;
Return RtnVal;
/END-FREE
P GetFnPtr E
This could be done more elegantly with APIs, but since all of the optional modules are known to the service routine at compile time, I wanted to make it simple and obvious to anyone reading the code.
Once the procedure pointer value has been set, all the main program needs to do is declare the imported procedure pointer and the dynamic function prototype:
D GetNozzle PR Like(T_NOZZLE)
D ExtProc(@GetNozzle)
D aSpread Like(T_SPREAD) Const
Now, the function named in the configuration variable (pGetNozzle) will be executed automatically by the mainline code when this line of code executes:
Since nozzles are manipulated only by the GetNozzle() function, there is no impact to the program as a whole. The AkGetNozzle() function is even free to call StdGetNozzle() and modify its result before returning to the main program. RPG doesn't support inheritance, but this is very close.
If there's a chance the @GetNozzle pointer might be null, it's a good idea to test for that before making the function call:
NewNozzle = GetNozzle(Nozzle.Spread);
Else;
// Do something to die gracefully
// because we're not configured correctly.
EndIf;
It's not a bad trade-off for a lot of flexibility and virtually no additional complexity. Now, the main program can happily ignore the whole nozzle problem and get some work done.
Pete Hall is a Development Analyst with Virtual Care Provider, Inc. He lives in Wisconsin. He can be reached by email at
LATEST COMMENTS
MC Press Online