A Style Guide for Modern RPG and ILE, Part 2
October 18, 2016 Paul Tuohy
One of the basic principles of programming is that coding conventions (guidelines and standards) improve the readability of source code and make software maintenance easier. Coding conventions provide the foundation for developing applications that are easy to maintain and modify. This article completes the style guide to coding RPG programs using free-form RPG in an ILE environment started in A Style Guide For Modern RPG And ILE, Part 1.
When using free-form RPG (and nothing but free-form RPG), a lot of the old RPG “functionality” is no longer available (e.g., operation codes such as MOVE, MOVEL, GOTO, ADD, SUB, condi-tioning and resulting indicators). But there are still some old features that are available but should be avoided.
RPG’s Built-In Indicators
The use of RPG’s numeric indicators (*IN01 to *IN99) and special indicator (*INU1 to *INU8) should be avoided at all costs. They are not self-explanatory and are dependent on comments to clarify their usage.
Define your own indicators when a Boolean condition is required. Better to have something like this:
As opposed to this:
If the program has externally described display files or print files, which make use of numeric conditioning and/or resulting indicators, then define an indicator data structure to remap the numeric indicators in the display or print file to indicators with meaningful names in the program.
dcl-F Mod30101D workstn(*ext) usage(*input:*output) IndDs(WSI); dcl-Ds WSI qualified; F3Exit ind pos(3); F5Refresh ind pos(5); F12Cancel ind pos(12); F23Delete ind pos(23); pageDown ind pos(26); pageUp ind pos(27); errorInds char(10) pos(31); enableDelete ind pos(41); SFLInds char(3) pos(51); SFLDsp ind pos(51); SFLDspCtl ind pos(52); SFLClr ind pos(53); SFLNxtChg ind pos(54); SFLPageDown ind pos(55); SFLPageUp ind pos(56); SFLProtect ind pos(57); enableMsgSFL ind pos(91) inz(*on); end-Ds;
Compile Time Arrays
The definition of a compile time array means that the definition of the array (in the data declarations), is separate from the definition of the data (which is at the end of the program).
dcl-S monthNames char(9) dim(12) ctData perRcd(3); // (lots and lots and lots of code here) **CTDATA MonthNames January February March April May June July August September October November December
It is better to define an array and its corresponding data in a data structure–the definition of the data structure overlaying the definition of the data.
dcl-Ds allDays; *N char(9) inz('January'); *N char(9) inz('February'); *N char(9) inz('March'); *N char(9) inz('April'); *N char(9) inz('May'); *N char(9) inz('June'); *N char(9) inz('July'); *N char(9) inz('August'); *N char(9) inz('September'); *N char(9) inz('October'); *N char(9) inz('November'); *N char(9) inz('December'); monthNames char(9) dim(12) pos(1); end-Ds;
The definition of an array in the same location as the data makes it easier to modularize code into subprocedures when required.
Multiple Occurrence Data Structures
Data structure arrays should be used in place of multiple occurrence data structures. Multiple occurrence data structures only allow access to one occurrence (element) at a time (e.g., cannot directly compare two occurrences of a multiple occurrence data structure) and setting the required occurrence (OCCUR/%OCCURS() ) is cumbersome, as opposed to simply using an index to identify an array element.
Apart from the standard style guidelines, there are a few guidelines that relate specifically to embedded SQL.
As mentioned earlier, global definitions should be kept to a minimum.
The most valid candidates for global definitions are file and/or data area definitions. If they are being used, they should be modularized within a module with the subprocedures that will be processing them. Consider using qualified data structures for all I/O.
If global variables are being defined, they should be qualified (either in a data structure or with a prefix such as g_) so they are easily identified as global variables within a subprocedure.
The use of the IMPORT and EXPORT keywords for variables should only be used as a last resort and should be well documented when used.
Parameters, Prototyping, and Procedure Interfaces
As well as providing a means of validating parameter definition at compile time, prototyping and procedure interfaces allow you to specify how parameters are used.
Remember, prototypes are only required for external subprocedure or program calls. They are not required for subprocedures that are coded and only called within the same module/program.
If the value of a parameter is not changed by a subprocedure/program, use the VALUE or CONST keywords to indicate that such is the case. These stop parameters from being inadvertently changed by a subprocedure or program.
Subprocedures can be used to code procedures, which do not return a value, and functions, which do return a value. (A call to a program would be considered a procedure call, since programs cannot return a value.) One of the items that is often debated is whether or not subprocedures should always return a value.
The argument for always returning a value is that there is always a value to return. For example, even if a procedure does not calculate and return a specific value, should it return a value to indicate whether or not the process worked?
As a preferred practice, don’t force procedures to return a value.
All prototypes should be coded in copy members and included in required programs and modules. A prototype should never be coded in more than one source member.
The copy members that contain the definition of the prototypes should also contain the definition of any templates and named constants that refer specifically to the use of the corresponding prototypes.
Managing the maintenance and usage of the prototype members can be quite a challenge and, at the moment, there is no single simple solution. Every solution comes with a cost.
The prototype members must be easy to maintain. You do not want a situation where two programmers need to change prototype definitions in the same member at the same time. This means that there will usually be a one-to-one correspondence between prototype members and modules, i.e., a prototype member containing all prototypes, templates, and named constants for subprocedures in a corresponding module. The same one-to-one correspondence would apply to prototypes for program calls although a “program” prototype member might contain prototypes for a group of related programs.
The ease of maintenance requirement means there will be quite a few prototype members. There is now the challenge of how the programmer knows which members to include in a program when a call is to be made to a program or an external subprocedure. This is even more challenging if the prototype members are in a source physical file with a 10-character naming restriction, there is little chance of having meaningful names. An alternative is to define prototype members in directories in the Integrated File System (IFS), where meaningful names can be given to the prototype members.
Compare the following directive to include a prototype member from a source file:
With the corresponding directive for a file in the IFS:
The requirement for /INCLUDE directives can be reduced by using nested copies. It is possible to have a single /INCLUDE directive that would include all prototype members in the application. Whereas this approach is appealing (the programmer only needs to know the name of one copy member), there are a number of considerations:
The following example shows one approach to using nested copies to minimize the requirement for multiple /INCLUDE directives. A program contains the following /INCLUDE directive:
The BASEINFO member contains nested include directives, as follows:
/include utility,pUtility /include fileProcs,protoFile /include common,commproto /include genstat,pStatGen /include regFunc,pRegFunc //-------------------------------------------------------- // Include CGI Prototypes, if required /If Defined(CGIPGM) /include CGIDEV2/QRPGLESRC,PrototypeB /include CGIDEV2/QRPGLESRC,Usec /EndIf //-------------------------------------------------------- // Include HSSF Prototypes, if required /If Defined(HSSF) /include SIDSrc,HSSF_H /EndIf
Each of the included members might contain prototype definitions or might contain further nested include directives. For example, the PUTILITY member, in turn, contains nested include directives, as follows:
/include utility,PUTILMSGS /include utility,PUTILCGI /include utility,PUTILSPACE /include utility,PUTILIFS /include utility,PUTILDATE
The members PUTILMSGS, PUTILCGI, PUTILSPACE, PUTILIFS, and PUTILDATE contain the actual prototypes.
We can alleviate the overhead on the refresh of RDi’s Outline view by using compiler directives to indicate what should and should not be included. Changing the definition of the PUTILITY member as follows means that a definition name must be set in order for the prototypes to be included.
/if defined(UTILITY) /define UTIL_MESSAGE /define UTIL_CGI /define UTIL_USERSPACE /define UTIL_IFS /define UTIL_DATES /endIf /if defined(UTIL_MESSAGE) /include utility,PUTILMSGS /endIf /if defined(UTIL_CGI) /include utility,PUTILCGI /endIf /if defined(UTIL_USERSPACE) /include utility,PUTILSPACE /endIf /if defined(UTIL_IFS) /include utility,PUTILIFS /endIf /if defined(UTIL_DATES) /include utility,PUTILDATE /endIf
The originating program (that includes the member BASEINFO) can set the required definition names or can define UTILITY, which would result in all members being included.
/define UTIL_MESSAGE /define UTIL_USERSPACE /include common,baseInfo
The “difficulty” with this approach is that the programmer must know the required definition names–so documentation is required.
The benefit of this approach is that it documents what is being included in the program.
Note that in this example, definition names are being used to include single members, but they could just as easily be used to include multiple members (as with CGIPGM in the BASEINFO member).
As stated earlier, there is no simple solution to managing the maintenance and usage of the prototype members. The challenge is to find which combination of techniques will provide the best balance between ease of maintenance and ease of use.
The Integrated Language Environment
There are many approaches to setting standards for the Integrated Language Environment (ILE). Common questions are:
Unfortunately, the answer to all of these questions is: It depends. The structure of an ILE application depends on the application and what it does.
One example of how to structure an ILE application can be found in my article Development Environments.
A change management system is something that can have a major impact on the methodology you use when developing an ILE application in that the change management system may have a preferred technique for managing service programs, binder language, etc.
Regardless of the final development environment, these are some of the dos and don’ts for an ILE environment.
Although the structure of ILE programs can be more complicated (one or more modules, binding to service programs), you still want the process of creating a program to be a simple one. In order to compile a program, the programmer should not need to know about activation groups and what all the service programs are, let alone what subprocedures are in what service programs.
This can be achieved through the diligent use of binding directories and a standard control specification, which gets included in every program via an /INCLUDE directive.
Ctl-Opt debug datEdit(*MDY/) option(*srcStmt:*noDebugIO) bndDir('MYAPP'); /if defined(*CRTBNDRPG) Ctl-Opt dftActGrp(*no) actGrp('MYACTGRP'); /endIf
Service programs are at the core of any ILE application. If a subprocedure can be used in more than one place, then it belongs in a service program.
Since content of a service program can be called from multiple places, the management of a service program requires a bit more care than “normal” programs. The approach to managing changes to a service program should be along the same lines as the way changes to a database are managed. As with a database, the minimum of people should have the ability to make changes. Any programmer can develop a subprocedure, but not any programmer can incorporate it in a service program.
Here are some basic pointers for service programs:
When creating an ILE program or service program, binding directories provide a means of generically providing a list of modules and service programs that may be required for binding.
Binding directories should be kept to a minimum. A single binding directory should list all service programs that might be required when a standard program is created.
Depending on the complexity and cross referencing between service programs, each service program might also require a binding directory. If there are multiple ILE applications and there are service programs and modules that are common to all, then another binding directory may be used to list those objects.
Remember, a list of binding directories can be included in the BNDDIR keyword on an RPG control spec.
There have been cases where an application has one binding directory per program–the binding directory lists the modules and service programs required to define the program. This is a mistake and imposes a maintenance overhead that is not required.
Managing service programs with many modules has to be done either by having a specific binding directory for the service program, by writing a CL program to create the service program, or by using a generic name for all modules in the service program.
Generally speaking, an activation group applies to an application. At times, a separate activation group might be used if scoping for commitment control and/or file overrides.
ILE programs should never be run in the default activation group. This can only be achieved if an ILE program is created with an activation group of *CALLER and the program is called by an OPM program (or from a command line). It is best to use the name of the activation group when creating programs. This is easily achieved using the ACTGRP keyword in a standard Control Spec in the programs.
Even worse than having an ILE program in the default activation group is having a service program in the default activation group. This can only happen if an ILE program is running in the default activation group. Again, ILE programs should never be run in the default activation group.
Also, avoid using the QILE activation group. This is the activation group that is used when care has not been taken to choose the activation group. If care was not taken to choose the activation group, care may not have been taken in other aspects of the application, and your application may be subject to the activation group ending unexpectedly, or to overrides and commitment control actions that you do not want.
Roll Your Own
Hopefully, these guidelines will provide a starting point for setting your own guidelines and standards. Another excellent source for style guidelines may be found in Appendix D of “Pro-gramming in RPG, 5th Edition” by Jim Buck and Bryan Meyers. Please let me know if you have any comments or additions.
Paul Tuohy is CEO of ComCon, an iSeries consulting company, and is one of the co-founders of System i Developer, which hosts the RPG & DB2 Summit conferences. He is an award-winning speaker who also speaks regularly at COMMON conferences, and is the author of “Re-engineering RPG Legacy Applications,” “The Programmers Guide to iSeries Navigator,” and the self-study course called “iSeries Navigator for Programmers.” Send your questions or comments for Paul to Ted Holt via the IT Jungle Contact page.