As parameter lists get more complex, making parameters optional gets harder, but service programs can help.
This series is all about encapsulating your business logic. In the first article in this series, I showed you how to use optional parameters to make your called program a little more flexible. This technique works very well in simpler cases when you have just a few parameters and some are used less frequently than others. However, optional parameters become a bit more cumbersome when the number of parameters grows or when any combination of optional parameters can be passed. This article presents a different approach that can handle any variety of parameters.
Moving Away from Optional Parameters
Let's review our parameter list from last time.
d OrdExp pi
d iOutputFile 128 varying
d iNumOrders 6s 0
d iBegOrder like(OHORDNUM) options(*nopass)
d iEndOrder like(OHORDNUM) options(*nopass)
d iCustomer like(OHCUSNUM) options(*nopass)
d wBegOrder s like(iBegOrder) inz(0)
d wEndOrder s like(iEndOrder) inz(*hival)
d wCustomer s like(iCustomer) inz(0)
/free
if %parms > 2;
wBegOrder = iBegOrder;
if %parms > 3;
wEndOrder = iEndOrder;
if %parms > 4;
wCustomer = iCustomer;
endif;
endif;
endif;
If you recall, we took special care to order our parameters so that required input and returned parameters were first, followed by optional parameters. Then we created work variables for each of the input parameters with appropriate initial values, and finally we checked the number of parameters passed to determine whether to override the initial values with parameters passed in. While it is a nice concept, it does tend to be a little inflexible, and it also requires some setup work. That work grows as you add more parameters. Picture adding some new variables. Say we wanted to select only orders of a specific type, allow for selecting a specify state or zip code, and also add a yes/no flag to include only open orders. The parameter list would look like this:
d OrdExp pi
d iOutputFile 128 varying
d iNumOrders 6s 0
d iBegOrder like(OHORDNUM) options(*nopass)
d iEndOrder like(OHORDNUM) options(*nopass)
d iCustomer like(OHCUSNUM) options(*nopass)
d iType like(OHORDTYP) options(*nopass)
d iState like(OHSTATE) options(*nopass)
d iZipCode like(OHPSTCOD) options(*nopass)
d iOnlyOpen n options(*nopass)
If you look at the parameter list, you'll notice that in order to specify zip code, I also have to specify state code, which is unnecessary. This isn't the only situation like this either; any mutually exclusive parameters get sort of ugly. There's no way to order them so that you only specify one or the other. Not to mention the fact that I have to add more work variables and extend my %parms test code. So that brings me to the next option: the service program.
Using a Service Program
What we're going to do is use a service program to provide multiple "wrappers" or interfaces to the called program, but with different parameter lists. In the object-oriented world, this sort of interface is called an "adapter" because that's exactly what it does: it adapts one parameter list to another. I'll write a service program that will have multiple procedures with only those parameters that make sense for that procedure. For example, ExportOrderRange might have beginning and ending orders, while ExportOpenCustomerOrders will need a customer number. Let's see how that works:
h nomain
d ORDHDR e ds template
d ExportOrderRange...
d pr 6s 0 extproc('ExportOrderRange')
d iOutputFile 128 varying
d iBegOrder like(OHORDNUM)
d iEndOrder like(OHORDNUM)
d ExportOpenCustomerOrders...
d pr 6s 0 extproc('ExportOpenCustomerOrders')
d iOutputFile 128 varying
d iCustomer like(OHCUSNUM) const
d OrdExp pr extpgm('ORDEXP')
d iOutputFile 128 varying const
d iNumOrders 6s 0
d iBegOrder like(OHORDNUM) const
d iEndOrder like(OHORDNUM) const
d iCustomer like(OHCUSNUM) const
d iType like(OHORDTYP) const
d iState like(OHSTATE) const
d iZipCode like(OHPSTCOD) const
d iOnlyOpen n const
p ExportOrderRange...
p b export
d pi 6s 0
d iOutputFile 128 varying const
d iBegOrder like(OHORDNUM) const
d iEndOrder like(OHORDNUM) const
d wNumOrders s 6s 0
/free
OrdExp(iOutputFile: wNumOrders: iBegOrder: iEndOrder:
0: ' ': ' ': ' ': *off);
return wNumOrders;
/end-free
p e
p ExportOpenCustomerOrders...
p b export
d pi 6s 0
d iOutputFile 128 varying const
d iCustomer like(OHCUSNUM) const
d wNumOrders s 6s 0
/free
OrdExp(iOutputFile: wNumOrders: 0: *hival: iCustomer:
' ': ' ': ' ': *on);
return wNumOrders;
/end-free
p e
You'll see that the service program starts out with a NOMAIN statement as all service programs do. Next is a template used to bring in the field definitions of the ORDHDR file, which are used to define the various entries in the parameter lists.
Next is the prototype for the procedure ExportOrderRange. This procedure is designed specifically to export orders within a range of order numbers. Besides the order range, the only other field we need to pass is the export filename. So we define the procedure to take as input values the export file name and the beginning and ending order numbers. One thing you might notice is that the number of records has been transformed from an input/output parameter to the procedure return value. This is a standard technique that often allows you to use fewer temporary variables. If you only return one value, this is a good technique to use. The other common use for the return value is to return the completion code. Anything other than a successful completion and the caller can call another procedure to get more detailed information if needed. That topic is more appropriate in an article focused on service program design, so let's move on with this example.
You see another prototype in this case for the procedure ExportOpenCustomerOrders. It defines only the export filename and the customer number as input parameters. You may have noticed that I also specify an extproc keyword, which looks to be redundantly defining the procedure name. Don't be alarmed; that's working around an RPG convention. Traditionally, all names in RPG are uppercase, so when a procedure is exported, RPG converts the name to all uppercase. Being all spiffy and modern now, I prefer to use mixed case whenever possible, so I use the extproc keyword to override that behavior and export my procedure name in mixed case.
After the internal prototypes comes the prototype for the original RPG program that this service program will be calling. I don't have to use *NOPASS anymore, and I define all of the input parameters as constants with the CONST keyword. This allows me to pass them as literals in the procedures. Let's get on to those.
Take a look at ExportOrderRange. As already noted, the procedure takes three input parameters: the file name and the beginning and ending order numbers. Notice how the procedure passes those values through to the original program and then specifies default values for all the other parameters. That's what makes this whole technique work: the service program hides the complex parameter list from the calling program. You'll see something similar in ExportOpenCustomerOrders. In this case, the only parameter other than the export file name is the customer number. Everything else gets hardcoded during the call to OrdExp. Note that I am able to use *HIVAL to specify the ending order number as well as pass *ON into the iOnlyOpen parameter, directing the called program to include only open orders.
The Result
Here's how you write a program to call one of those procedures.
d ORDHDR e ds
d ExportOpenCustomerOrders...
d pr 6s 0 extproc('ExportOpenCustomerOrders')
d iOutputFile 128 varying const
d iCustomer like(OHCUSNUM) const
d wNumOrders s 6s 0
/free
wNumOrders = ExportOpenCustomerOrders( 'yothere.com': 123456);
*inlr = *on;
/end-free
As you can see, this program has no idea that the called program, ORDEXP, has nine parameters. As far as the program is concerned, it only passes in the output filename and the customer number, and one or both can even be literals. No messy work variables for unused parameters; the service program does all that for you.
Obviously, you can write as many of these procedures as makes sense. Just identify the most common combinations of parameters and call the procedure that way. And for unique circumstances, you can just prototype the ORDEXP program itself (using the same procedure found in the service program) and pass in all the variables. But the code above is simple, easy to read, and easy to maintain. Which is what we're looking for!
LATEST COMMENTS
MC Press Online