This strategy is, of course, completely voluntary. Should you choose to use it, you must adhere to programming standards and guidelines for maximizing reusability and ensuring consistent programming interfaces.
Binding Contracts
Object-oriented (OO) languages like Java and C++ enforce consistent interfaces by creating a hierarchy of abstract classes and interfaces that must be implemented in order to properly create an object of the desired type with the desired functionality. Since RPG IV is not an OO language, you must find creative ways to enforce an API and thereby ensure that functioning, concrete methods will be available (in your program) for callback processing (similar to callback processing requirements of the SAX API for parsing XML documents, as described in "Fast XML with RPG IV and SAX") from standard reusable services that exist in your API. This is referred to as establishing and enforcing contracts between a service requester and service provider (in this case, bound modules or service programs).
Contracts make reusability and interfaces work by requiring two-way or reciprocal binding commitments between a service requester and a service provider. They ensure that a generic utility or service provider can customize its behavior to meet client application requests by issuing callbacks to requester application functions. The requester application functions are enforced by dependent calls from the service provider (that must be resolved at bind time for successful compilation) to the service requester, to obtain application-specific information. This type of give-and-take exchange is best illustrated in Figure 1, which is discussed and shown later in this article.
Managing Consistency
An alternative (for providing a similar level of service) would be to pass an extensive list of arguments to the service provider when the initial callout request is first made. However, this adds the burden of passing those arguments down through successive calls to other functions that may not need them (or establishing undesirable global variables to retain the data) so that the information is available to the actual servicing functions when and if such services are needed. Besides creating added complexity to the process of maintaining and debugging the application, this strategy imposes no requirement to ensure that programs bind to all those services expected--and in some cases required--to deliver even a minimal set of program features and functionality. This could translate into inconsistent features or feature behavior in programs in the same application group, and consistency is a desirable objective in most development activities.
This is why abstract class and interface constructs have been developed and are so important in Java and C++ worlds and why COM/COM+ and JNI exist to manage interfaces in a mixed-language development environment--to enforce feature consistency and behavior and to improve reusability. "This is fine and dandy," you say, "but how do I translate that into practical terms that can be implemented into my daily RPG IV or C programming requirements?"
ILE--More Than IV %BIFs
First, you must convince yourself and others in your programming team that developing standard and consistent interface requirements is a task worth investing your time and future development on. If RPG IV or any other procedural languages assume a vital role in your future development activities, half the battle is won. The other half of the battle depends on your commitment to ILE beyond simple source conversion from RPG/400 to RPG IV. If your shop is content to exploit the benefits of new BIFs and the occasional new opcode but doesn't really care about developing consistent APIs that ensure reusability (beyond source), isn't interested in improving productivity (by reusing tested and proven components), and doesn't anticipate the need for mixed-language development, then this strategy is probably not for you.
However, if you are seeking a strategy that makes the most effective use of ILE and want to develop applications that move toward the style and structure of languages such as Java and C++, you will want to establish a practical approach for when and how to use the techniques you will learn from this article. For example, attempting to implement this strategy into legacy code that is barely RPG III much less ILE RPG IV is probably not a good idea. But new development projects that have a "standalone" quality about them are ideal for starting to work with this type of strategy. While conditions do not have to be ideal for using the techniques learned in this article, it is important to recognize that some projects are not suitable for interface implementations--such as applications that are locked to a vendor-controlled code base or applications that are so tightly integrated that the benefits of interface implementations cannot be fully appreciated without rewriting the entire application. Just as with integrating any new technology, you must size the projects. Weigh the expected turnaround and cost vs. benefits expected by users. Making the assumption that you have such a project, for purposes of this article, what techniques would bring you closer to interface enforcement?
Let's Shake on That!
Interface requirements, in the current state of ILE RPG IV applications development, can be ensured by establishing, maintaining, and--through programming standards and guidelines--enforcing reciprocal (handshake) binary contracts using well-thought-out and well-constructed sequence diagrams and binding directories that follow consistent application binding strategies. In Figure 1, you see a simple scenario in which such a concept is put to work.
Figure 1: Here's an example of interfaces enforced by binding resolutions.
In the example in Figure 1, you have a requirement to print letters for W2 recipients (employees) and 1099 recipients (vendors), reminding them that their taxable compensation has exceeded provisions for withholdings so that they can make quarterly estimated payments (or suffer additional interest and penalties). In the example for this article, you will only be implementing the IReminder interface for 1099 reminder letters because it is simple to follow and makes the point.
As you can see from Figure 1, a service program (*SRVPGM) called Reminders resides in the Letters application group binding directory. It provides the reminder letter printing function, print_reminder(), by consecutively calling three other functions in that module, print_opening(), print_body(), and print_closing(). Each of these functions, in turn, issues a callback to the requester (in this case TEST1099) to the get_addressee(), get_taxable(), and get_approval() functions, respectively, to extract the data necessary to fill in the required portions of the standard letter format for reminders. This format is shown later in this article as compile-time table elements in the Figure 4 section [K] source.
Establishing interface requirements in this manner not only creates a handshake requirement between the requester of a service and the rendering service provider, but it also enables information (data) to reside in the appropriate modules that should know about and handle such information. This, in turn, allows service modules that render a generic or common service to remain relatively pure in purpose and functionality, which bodes well for stability and reliability of the service. Figures 2, 3, and 4 show this concept in more concrete terms with a source code example.
|
Figure 2: This is the IReminder interface in the abstract.
Step by Step
Figure 1 shows a sequence diagram depicting the messages (function calls) between the requester and service provider for reminder letters. Figure 2 shows the source code representation of the IReminder interface depicted in Figure 1. Figure 2 section [A] defines two constants that represent the supported reminder letter types (W2 and 1099). Of course, in this example, you will only implement the 1099 reminder letter type. [B] and [C] are mutually exclusive versions of the IReminder data structure, as depicted by the /if-defined(IMPLEMENTS_IREMINDER)/else-/endif. The [B] version is copied during compilation into those program source members that implement the IReminder interface, comprised of 2: get_addressee, 3: get_taxable, and 4: get_approval functions depicted in Figure 1. The [C] version is copied during compilation into the service provider that requires the callback services rendered by a properly implemented IReminder requester, to comprehensively service the 1: print_reminder function request issued by the requester.
As you can see, [B] in Figure 2 sets up a requirement for any requester implementing the IReminder interface by defining the initialization of three procedure pointers with the addresses for each of the three callback functions defined and depicted in the IReminder interface in Figure 1. This requirement ensures that the three functions (required by the service provider) will be implemented by the requester; otherwise, the program will fail to be created during the binding step and will result in messages like these:
Definition not found for symbol 'get_approval'.
Definition not found for symbol 'get_addressee'.
Figure 2 section [C] ensures the service provider uses the same IReminder interface definition as the requester providing the callback services by residing in the same /Copy member, but it omits the initialization of the procedure pointers since it will be the recipient of the requester's initialized version on the callout to print_reminder(), which takes an IReminder type as its only argument, as shown in [D] of Figure 2 prototyping the print_reminder function. Whereas [B] and [C] of Figure 2 establish a consistent definition of the IReminder interface, [D] provides the callout that initiates the handshake requirement between the requester and service provider. While failing to include a call to print_reminder() in TEST1099 shown in Figure 3 [J] and [K] will not cause TEST1099 to fail the compile and binding steps, failure to include the call will prevent successful results from being achieved--something that will not go unnoticed. The IReminder /copy establishes the ground rules for subsequent communication between the requester and service provider of reminder letter services.
|
Figure 3: TEST1099 is the IReminder implemented by the requester.
You can begin to see the IReminder interface in action by reviewing the source code of the TEST1099 program in Figure 3. Figure 3 section [A] establishes the binding requirements of the requester by having it run in its own unique activation group--as indicated by actgrp(*new)--and establishes QC2LE and LETTERS as the binding directories to search when resolving imports and function calls. Recall from previous articles in this series that actgrp(*new) is required to properly view output from the C printf function, which resides in a service program (QC2IO) in the QC2LE binding directory. The LETTERS binding directory will contain an entry to the Reminders service program that contains the print_reminder functionality you will see later in this article. Figure 3 section [B] shows how a requester states its intention to implement the IReminder interface by defining the IMPLEMENTS_IREMINDER external compiler variable just prior to the /copy definition of IReminder. This triggers the [B] version of IReminder in Figure 2 and ensures that it is copied into the requester source stream for compilation and binding.
Now, jumping right into the code, you will see that [C] of Figure 3 was the same Abstract Data Type (ADT), address, that was first introduced in the article, "What? RPG IV a Better C Than C?" as the basis of var, var1, and var2 variables shown in [F] of Figure 3. In [I] of Figure 3, var1 subfields are initialized with the values for the first 1099 reminder recipient, John Doe. Added to the ADT definition of the previous article is the balance field that you will use for taxable amount where required. Immediately below that code, in [J] of Figure 3, the address of var1 is used for the basing pointer (pVar) for var. Next, the Reminder_Type field, in the IReminder data structure, is set to the value of the constant field RT_1099. Finally, the print_reminder function is called with the single argument IReminder, which has already been initialized with the three pointers to each of the required functions (get_addressee, get_taxable, get_approval) used by the print_reminder function. Putting aside what print_reminder does, for a moment, you will see that [K] of Figure 3 does exactly the same for var2 as [I] and [J] did for var1, except this time for recipient Jane Doe.
Now, turn your attention to Figure 4 for an explanation of what print_reminder does and how it uses the three procedure pointers passed in Figure 3 sections [J] and [K] to access the needed functionality in TEST1099 to customize its behavior and accomplish its task--printing the 1099 reminder letter.
|
Figure 4: Print reminders service provider (Reminders)
The first major point to note in Figure 4, [B] is that the Reminders source module calls for the [C] version of IReminder in Figure 2 to be copied into the compile source stream by undefining the IMPLEMENTS_IREMINDER external compiler variable. This ensures that when the /copy (also shown in [B] of Figure 4) is encountered, it does not import the version of IReminder that would attempt to initialize the three procedure pointers and fail the compile. Since Figure 4 will be using the C printf function to print reminder letters to the display, Figure 4 section [A] shows the required binding directory, QC2LE, in the H-specification. Figure 4 [C] presents the lettxt table definition that contains the letter body, the buffer used for sprintf output, and the newline and tab definitions used in both of the printf and sprintf functions to format output.
Printing the Reminder
Now, jump to the print_reminder function implementation in Figure 4 section [E]. You will see first a test that ensures that only properly initialized procedure pointers have been passed in the reminderI argument of that procedure (just above [E]) that uses the same format of the IReminder data structure defined in Figure 2. If the test fails, a message will be printed to the display (using the C printf function) indicating a fatal error has occurred and control will return to TEST1099. Otherwise, control drops through the condition to execute the three subprocedures--print_opening, print_body, and print_closing in [F] of Figure 4--that constitute the print_reminder functionality. The prototypes for these functions are shown in [D] of Figure 4.
Just for Openers
The call to print_opening in [F] of Figure 4 passes control to [G] of Figure 4 to print the opening content for the reminder letters. The opening consists of a separator (asterisks), the reminder type title, the current date, and the addressee information. The addressee information is the focus of this discussion, because it is taken by issuing a callback to the get_addressee function in Figure 3 [L]. As shown in that function, the stream_printf, prototyped at Figure 3 [D], uses the C I/O function sprintf to print its results to a buffer or array of characters rather than to the screen as the printf function does. This allows the requester to determine the format it wishes the address to appear in rather than leaving that decision to the reminder letters service, thus allowing customization by the requester. In Figure 4, [H] the get_addressee function is called and returns a pointer to the buffer that contains the formatted address information for the current vendor assigned to the pVar pointer in [J[ and [K] of Figure 3.
How are you able to call functions that exist in TEST1099? When the initial call to print_reminder was made from one of the two codepoints in Figure 3 [J] and [K], the procedure pointers that were initialized (with the addresses of the three get functions) when TEST1099 was invoked were stored in the IReminder data structure related variables in [B] of Figure 2. With the first line of code executed in [E] of Figure 4, the Reminders' copy of IReminder (global data structure) is initialized with the values of those pointers. These pointers, in turn, are used in the /copy version of IReminder for Reminders to initialize the prototype function (pointer) identified by procedure pointer--rather than resolved external name--in Figure 2 [C] for each of the three required functions: get_addressee, get_taxable, and get_approval.
Just Fill In the Blanks
Next, the print_body function is called in Figure 4 [F] to print the body of the reminder letter from the table records in Figure 4 [K]. The second table record (after the asterisks separator line) requires the taxable amount to be "filled in" with the recipient's balance amount. This is accomplished by issuing a callback (shown in Figure 4 [ I ]) to the get_taxable function (shown in Figure 3 [M]) that simply returns the balance from the var data structure variable--whose basing pointer is set just prior to the call to the print_reminder function--to the appropriate recipient information variable (var1 or var2) address. Lastly, a For loop is entered to print the remainder of the reminder letter body lines in the lettxt table shown in Figure 4 [K].
Sincerely Yours
Finally, the print_closing function is called from Figure 4 [F] to print the closing of the standard reminder letter. In the example, this consists of two elements: the signoff word "Sincerely" followed by the name and signature of the department manager approving the reminder letter distribution. In this case, the manager was defined in Figure 3 [E] and initialized in Figure 3 [H]. Reviewing the source code in Figure 4 [J], you will see the callback issued to get_approval, defined in Figure 3 [N], which returns a pointer to the manager variable containing the name of the approving manager that is printed as the last line of the reminder letter. Control is then returned to the body of the print_reminder function, which then returns control to the requester (in this case, TEST1099).
The results from the example implementation in the IReminder interface by TEST1099 can be obtained by issuing a call to TEST1099 like the following:
CALL TEST1099
Your results should look exactly like that shown in Figure 5. To obtain these results, be sure to follow the build instructions included in the program banner of each source member.
Figure 5: This screen shows a sample result of running TEST1099.
What's Next?
The example given here was contrived to present a simple set of steps for defining and implementing interfaces in RPG IV. In practice, interfaces are usually much more extensive than the four functions depicted by Figure 1. However, this example does show you the basic principles and concerns for implementing interfaces. The companion downloadable source code for this article consists of not only the source presented, but also fixed-format versions (as denoted by an "@" attached to the end of the member name) of TEST1099 and Reminders (for those encumbered by an OS/400 version lower than V5) and the same functionality presented here as it would be coded in Java (Review1099). Comparing the Java and RPG IV free-form versions will give you a better appreciation and understanding of interfaces in an object-oriented language. But the story is not yet complete. If you want to see the ending, stay tuned to MC Mag Online for the last article in this series, "ILE RPG IV vs. C--The Final Conflict."
Jim Barnes is a freelance writer and independent consultant working in Houston, Texas. Jim can be reached at
LATEST COMMENTS
MC Press Online