Encapsulating File Access in a Service Program
July 21, 2004 Joel Cochran
The code for this article is available for download.
Encapsulation is a familiar concept to object-oriented programmers but has yet to be seriously embraced by RPG programmers. The idea is to place all access for a particular set of data, in this case from a physical file, to one object. Then only one object can directly access and manipulate a database file. While this is standard fare for OO languages, it is hardly the norm for RPG. In this article I demonstrate a method that simulates encapsulation by using our trusty friend the service program.
WHY ENCAPSULATION IS COOL
Let’s start off with a simple file of names and e-mail addresses based on the following DDS:
A UNIQUE A R MYNAMER TEXT('NAMES AND EMAILS ADDR A ID 5S 0 COLHDG('ID KEY') A USERNAME 35A COLHDG('USER NAME') A USEREMAIL 128A COLHDG('USER EMAIL ADDRESS' A K ID
Now assume that through years of maintenance and development our little file here has been used in over a hundred programs. After so much propagation, you need to verify that the format of the e-mail address is correct. User error causes problems that you can limit if you control how the data is entering the file.
This usually would mean finding all instances of updates and writes and adding validation logic in each place. How many times have you seen a subroutine like this copied all over the place? But since we are more modern RPG programmers, we could easily enough write a validateEmail() routine and stick it in a service program. This is probably what we should do, but it doesn’t change the fact that we still need to find all the instances that update or write to the file and add the call to the new subprocedure.
Another typical problem is adding a new field to the file, say a telephone number. It’s easy enough with DDS:
A UNIQUE A R MYNAMER TEXT('NAMES AND EMAILS ADDR A ID 5S 0 COLHDG('ID KEY') A USERNAME 35A COLHDG('USER NAME') A USEREMAIL 128A COLHDG('USER EMAIL ADDRESS' A PHONENUM 10S 0 COLHDG('USER PHONE' A K ID
As you probably already recognize, the problem here is the level check. You now have to find and recompile every program that accesses the file in order to prevent a runtime error. In a lot of software shops, database changes like this to legacy applications are practically forbidden because of such problems. In both examples, a lot of additional work has to be done to ensure the application keeps running without errors.
Now imagine if you had used encapsulation for this file. Either adding a field or instituting some rules would require changing and recompiling only a single source member; the magic of UPDSRVPGM would take care of the rest. From a maintenance perspective, that is very cool.
Another cool thing about encapsulation is that you can hide a lot of the nitty gritty details. If you write a service program for other programmers to use, you can minimize the amount of work they have to do in order to properly use the file, while allowing you to easily enforce your rules. Let’s add two more fields to the file above that represent the user profile that updated the record, and a timestamp:
A UNIQUE A R MYNAMER TEXT('NAMES AND EMAILS ADDR A ID 5S 0 COLHDG('ID KEY') A USERNAME 35A COLHDG('USER NAME') A USEREMAIL 128A COLHDG('USER EMAIL ADDRESS' A PHONENUM 10S 0 COLHDG('USER PHONE') A UPDUSER 10A COLHDG('UPDATE USER') A UPDSTAMP Z COLHDG('UPDATE TIMESTAMP') A K ID
Again, if you really wanted to enforce this, you would have to find all the write and update statements for this file and add these fields to the program logic. With encapsulation you only need to add this in one place. Even more appealing is that the programmer using your service program doesn’t even have to code for it: the encapsulating service program can make this an essentially automatic feature. I’ll demonstrate this later in the article.
THE PIECES OF THE PIE
There are some standard procedures that need to be included in your encapsulating service program. These procedures break down into two categories. First, you need file access procedures for reading, updating, inserting, and deleting. Second, you need accessor procedures for the individual fields. Because your file access is hidden within your encapsulating service program, you will not be using file fields directly. As a result, you will need accessor procedures, also commonly referred to as getters and setters.
Before proceeding, I would like to point out that the example being built in this article uses SQL for all file access. This method can be easily adopted for native file access as well, but requires additional coding. If you are going to use native file access, I recommend you control the open and close status of the file and check in every access procedure for the file status.
FILE ACCESS PROCEDURES
You are going to rely on the global properties of your service program to hold file data. The simplest way to do this is to create an externally defined data structure based on your file. I prefer to name the data structure the same as the file and add a prefix.
h nomain d mynames e ds extname(MYNAMES) prefix(n_) * Prototypes /copy qrpglesrc,cp_protos d PSDS sds d userID 358 367
Note that this code snippet also includes H-specs, the /copy for your prototypes, and a program status data structure. Since we are still in the global section, I’m going to create some additional global variables that may be useful in the future.
* Global Variables d sql s 32767a varying d fileName s 20a varying inz( 'MYNAMES' )
Now you can begin building your procedures. I’m going to take advantage of RPG IV’s long names and prefix all my procedures with the file name. This helps prevent any namespace collisions and increases self-documentation in calling programs.
The first procedure populates the data structure based on the key field, “ID”. This is typically the first procedure called and effectively creates an “instance” of mynames. Once called, the data is available to all the other procedures in the service program.
*--------------------------------------------------------------------- * Select record and populate DS *--------------------------------------------------------------------- p mynames_getRecord... p b export d mynames_getRecord... d pi n d id 5s 0 const d isFound s n inz(*off) /free mynames_clear(); /end-free c/exec sql c+ select * c+ into :mynames c+ from MYNAMES c+ where ID = :id c/end-exec /free if SQLSTT = '00000' ; isFound = *on ; endif ; return isFound ; /end-free p mynames_getRecord... p e
The procedure returns an indicator, letting you know whether the selection was successful. This makes it very easy to employ branching logic in the calling code if the record is not found. You’ll also notice that the first thing this procedure does is to call another procedure in the same service program, mynames_clear(), which clears the data structure. This is to prevent any residual data problems should the record not be found. This is a useful procedure before inserting records as well, so we’ll go ahead and export it:
*--------------------------------------------------------------------- * Clear the mynames DS *--------------------------------------------------------------------- p mynames_clear b export d mynames_clear pi /free clear mynames ; return ; /end-free p mynames_clear e
Updating and deleting records are very similar to each other. Below is the procedure for updating a record.
*--------------------------------------------------------------------- * Updating the mynames file *--------------------------------------------------------------------- p mynames_update b export d mynames_update pi d isUpdated s n inz(*off) /free n_UPDUSER = userID ; n_UPDSTAMP = %timestamp(); /end-free c/exec sql c+ update mynames c+ set USERNAME = :n_USERNAME, c+ USEREMAIL = :n_USEREMAIL, c+ PHONENUM = :n_PHONENUM, c+ UPDUSER = :n_UPDUSER, c+ UPDSTAMP = :n_UPDSTAMP c+ where ID = :n_ID c/end-exec /free if SQLSTT = '00000' ; isUpdated = *on ; endif ; return isUpdated ; /end-free p mynames_update e
The first thing you have done is made the user name and timestamp information in the file “automatic.” By placing this code in the actual update procedure, you are enforcing our rule. Examining this procedure shows you are using the current values in the data structure to update the record. This mimics classic native file access. Adding procedures for deleting and inserting follows the same model.
FIELD VALUE ACCESS AND USE
To read and populate the data structure subfields, you need a series of getters and setters. These very small procedures become the most significant because they actually provide the program interface for the field values. From a design standpoint, they could not be simpler. Below is an example of getting and setting the USEREMAIL field value:
*--------------------------------------------------------------------- * Get USEREMAIL *--------------------------------------------------------------------- p mynames_getUserEmail... p b export d mynames_getUserEmail... d pi 128a /free return n_USEREMAIL ; /end-free p mynames_getUserEmail... p e *--------------------------------------------------------------------- * Set USEREMAIL *--------------------------------------------------------------------- p mynames_setUserEmail... p b export d mynames_setUserEmail... d pi d userEmail 128a const /free n_USEREMAIL = userEmail ; return ; /end-free p mynames_setUserEmail... p e
Include a getter procedure for every field you want to be able to retrieve, and a setter procedure for each one you want to be able to change. A good rule of thumb is to make the return values and the input parameters the same definition as the fields you are accessing. If you examine the complete source code for this article, you will notice that there are no setter procedures for the ID, UPDUSER, or UPDSTAMP fields. That’s because I want to control how those values are generated and assigned.
Looking at the mynames_setEmail() procedure above, if you wanted to institute the e-mail address validation mentioned earlier in this article, this is where you would do so. You could return an indicator, or an error code, or a message. You could easily add logic to the update and insert routines that would not allow the operation to occur if the e-mail wasn’t valid. Those implementation details are well beyond the scope of this article, but the concept is clear: you have a significant amount of control inside this one service program to enforce your database rules and maintain data integrity.
USING THE SERVICE PROGRAM
You will need to create the prototypes and binder source and compile the *MODULE. Then you can use the *MODULE to create the service program and bind it to a program. Here is a brief example:
d mynames e ds extname(mynames) /copy qrpglesrc,cp_protos d message s 50a inz('User Added.') /free mynames_clear(); mynames_setUserName( 'JOEL' ); mynames_setUserEmail( 'jcochran@itjungle.com'); if mynames_insert() ; dsply message ; endif ; mynames_getRecord( 1 ); USERNAME = mynames_getUserName(); dsply USERNAME ; mynames_setUserName( 'RAYMOND' ); mynames_update(); USERNAME = mynames_getUserName(); dsply USERNAME ; *inlr = *on ; /end-free
FINAL THOUGHTS
Like any service program, you can include as many procedures as you like. In the example here, you may want to include a procedure for counting the number of records, or returning an array of domain names from the e-mail addresses, or finding the most recently updated record. This design is replete with possibilities.
Also, be aware that nothing discussed up to this point would prevent someone from sidestepping your service program. This could probably be accomplished by using a trigger program, as could some of the data integrity checking discussed. However a lot of shops are wary of triggers, so this may not be an option for you. The best enforcement is internal policy and programmer discipline.
I’d like to point out that this is a simple example designed to work for a single record. Many things could be done to improve this program that are beyond the scope of this article. If I were going to put this into production, I would optimize the SQL, use prepared statements, and allow variable keys. I’d also add the capability to handle a group of records using a cursor and a fetchNext() procedure. Finally, I typically add a generic executeSQL procedure, using “execute immediate” as a catch-all for any oddball needs that may arise, as well as a getSQLstate procedure so that you could externally analyze any SQL errors.
Joel Cochran is the director of research and development for a small software firm in Staunton, Virginia, and is the author and publisher of www.RPGNext.com. E-mail: jcochran@itjungle.com
This article has been corrected since it was first published. The file CP-PROTOS.RPG was omitted from the download package. Guild Companies regrets the error. [Correction made 7/23/04.]