As you begin taking advantage of ILE and breaking your applications into small pieces, another problem may start to appear. Procedures start to reference, and become referenced by, so many other procedures that a simple change ends up affecting dozens of other procedures. A Mediator is a design pattern that helps to reduce this interdependence by providing a middle layer that prevents procedures from referring to each other directly.
If you were asked to write a set of easy-to-use I/O procedures with the flexibility and power of embedded SQL, you might respond, Ill have it for you as soon as we get that Y10K problem licked. After you finish reading this article, you can optimistically reply, Sure, Ill use a Mediator. The key is in the Mediator, which takes requests from an application program and delegates them to supporting procedures. Mediators are not limited to supporting I/O; they can also be used to simplify functions such as editing numbers and display processing.
Mediators, as described in the book Design Patterns: Elements of Reusable Object- Oriented Software (see References below for publication information), define how objects interact with one another. When a Mediator is used, objects do not refer to each other directly. Instead, they refer to the Mediator that passes requests on to the object that supports the requested function. ILE and the dynamic linking program APIs make it much easier to create Mediators on the AS/400. These APIs allow you to create service programs that are referred to only at runtime. These service programs provide the same function that Dynamic Link Libraries (DLLs) provide to Windows programs.
One area that can benefit from the use of Mediators is I/O processing. Included in this article is an example of a simple I/O processing Mediator. (The code for the example in this article can be found on MCs Web site at http://midrangecomputing.com /mc/98/12.) Using this example as a base, a complete I/O processing subsystem can be created. A Mediator can help you exploit other areas, such as messaging, data validation, error handling, and display interaction.
My Applications Get Along; Why Do They Need a Mediator?
You do not need to use an object-oriented language to benefit from Mediators. A Mediator provides a single interface that encapsulates the behavior of many objects. The Mediator replaces many-to-many relationships that typically exist between clients and servers, with one-to-many relationships, which are simpler and more extensible. Many clients refer to the Mediator, which passes the requests on to many servers. This extra level of abstraction allows the clients and servers to vary independently.
Because clients and servers are independent, client procedures can be enhanced, or have their interfaces changed, without affecting server procedures. Conversely, server procedures can be enhanced without affecting client procedures. Another reason to use a Mediator is to reduce the interaction complexity between clients and servers. The complexity is moved to the Mediator, which prepares client input or combines and enhances server functions.
Mediated I/O
Mediators that are used to support I/O can map input and output buffers, set error flags, and return messages. The dynamic data modules demonstrate how a Mediator can be used to support I/O tasks. In this example, the tasks are OpnFil and GetRcd. These tasks can be used as a base to create a comprehensive I/O system.
Normally, there would be many clients and servers connected by a Mediator. The I/O server modules would be similar, containing the same procedures. The I/O server modules could even be generated using a description of the file that each server module supports. The dynamic data example contains a single client and server procedure. They are called DDClient and DDQDBFDEP. The Mediator in this example is DDMediator. The server could be used to support more than one file as long as the files formats are the same, but I have included only one file in this example.
Several of the procedures and prototypes included in the Mediator module in this example could be moved to a module that better fits their purpose. I placed them in the Mediator to make it easier to create and run the dynamic data example. When you begin to create your own Mediators, you should move the subprocedures that are not related to I/O processing into a general-purpose service program.
Because the Mediator has no idea how many files will be opened and what fields will be requested, this information is stored in memory that is dynamically allocated. Because dynamically allocated memory is lost when an activation group is reclaimed, you may want to explore using a user space and user indexes created in QTEMP. When an open request is made, a handle is assigned. The handle is used to set an occurrence of a multiple- occurrence data structure that contains pointers to format, file, and field information.
One of the Mediators tasks is the mapping of data between the client and I/O server procedure. When the Mediator is called, key values that are passed are updated in the servers I/O buffer. If a record is read, the field values returned in the servers I/O buffer are mapped to the area passed by the client. To support this mapping, the server has a procedure that returns field and key information for the format and files that are supported.
Dynamically Importing Data and Procedures
The server itself stores information about the record format and files supported by the server. The server module uses the Export keyword on procedures and fields that are made available to the Mediator. Normally, exported procedures and data elements are copied or linked together when a program is compiled. This linking can also be done dynamically at runtime through the use of APIs. Runtime linking slows down the speed of calls, but in typical business applications, which are usually I/O bound, the impact of a dynamic call is very slight.
To access the servers exported variables, the server service program is activated by calling the Activate Bound Program (QleActBndPgm) API. One of the parameters passed to this API is a system pointer to a program or service program. The Mediator uses one of the C MI library functions, Resolve System Pointer (RSLVSP), to set the system pointer. Prior to calling this API, you must convert the object type *SRVPGM to its hex equivalent.
To do this, I call procedure CvtObjTyp, which uses the Convert Type (QLICVTTP) API. After the program is activated, the Get Export (QleGetExp) API is called to retrieve pointers to the exported variables and procedures used by the Mediator. The first time a format or file is used, the Mediator initializes information about the format and files that the server supports by calling the servers BufInf and KeyInf procedures. This information is reused on subsequent opens for the same format or file. (In this example, it is used only for the specific activation group. In the Mediator I created here, I store this information in user spaces/indexes in QTEMP, which crosses activation groups.)
Mapping the Buffers
Ideally, the formats of the input buffers in the client and I/O server would not have to match. Also, the client application should be able to select fields. The client should also determine to where data will be placed on input, and from where it will be taken on output.
In the I/O example, the Mediator maps the servers I/O buffer to the area passed from a client. When the Mediator receives an OpnFil request, the field buffer information is loaded into a table that contains each fields offset in the buffer and its size. This information is used to map data from the I/O buffer to the area provided by the client, which allows the client to pass a list of fields and receive their values at the location the client specifies. A table is also built that contains offsets and sizes for the files key fields.
To allow the format of the client and server buffers to vary independently, the I/O buffer in the server is based on an exported pointer, which allows the assignment to be made by the Mediator and simplifies the server program. Also, because the Mediator maps the buffer, the format of the buffer passed from the client does not have to match the format of the buffer as defined in the server. The Mediator retrieves a description of the file buffer from the server by calling a procedure that returns the available fields and their sizes.
When the Mediator receives a request to read a record, it first determines whether the client specified all fields (special value *) on the open and if the buffer is long enough for all fields. If these conditions are met, the Mediator assigns the servers I/O buffer directly to the area passed from the client. The servers I/O buffer can be the same size as or larger than the record format being serviced. Consequently, if a subset of fields was requested or the area passed from the client will not hold an entire record, the Mediator allocates dynamic storage and assigns that storage to the servers I/O buffer.
To assign the server buffer to the area passed from the client or to the allocated storage, the Mediator sets the exported pointer that the servers I/O buffer is based on. The Mediator then changes the exported pointer to point to the area passed from the client or the allocated storage. You might be thinking, Golly (or something else). Pointers are hard enough to understand; now, we have pointers to pointers! Dont worry. Once you have this code working, you wont need to worry about pointers in either your client applications or your server procedures.
After assigning the server buffer, the Mediator updates the buffer with the key values passed from the client. A C library function, MemCpy, is used to update the buffer using the key field information that was gathered when the file was opened.
After calling the server, the Mediator has to map the returned values to the area passed from the client. The table of field offsets and sizes that was built when the file was opened is used to update the area passed from the client with the values returned from the server. This mapping is not necessary if the Mediator mapped the server I/O buffer directly to the area passed from the client.
Using the Client
The DDClient demonstrates the use of a Mediator by opening a file and then reading several records. Information from the records that are read is displayed using the DISPLAY op code. The client calls the Mediators OpnFil and GetRcd procedures. Because RPG IV does not support variable-type parameters, key values are passed using pointers. Instead of passing the key directly, the client passes a pointer to the key value. To do this, use %ADDR(KeyFld). Another limitation of RPG IV is the inability to pass variable-sized data
structures (because operational descriptors are not supported for data structures). To get around this limitation, I defined a field that is based on a pointer that is initialized to the address of a data structure.
To create the client, you need to create the DDClient and DDMediator modules. You will also need to create the MSGTKT service program (see RPG Building Blocks: ILE Message Handling, MC, November 1998). Use the Create Program (CRTPGM) command with modules DDClient, DDMediator, service program MSGTKT, and the binding directory QC2LE. After you have created the client program, you will need to create the server service program. Use the Create Service Program (CRTSRVPGM) command and specify module DDQDBFDEP and EXPORT(*ALL).
After the client program and server service program are created, you can run the DDClient program. The library that contains the server service program needs to be in your library list. When you call the DDClient program, you should see a list of fields related to the file QADBFDEP. To better understand how the Mediator works, you may want to start Debug and step through the program as it executes.
One thing you may notice when you step through the Mediator in Debug is that some of the variables cannot be displayed using the debuggers EVAL operation. This is caused by a debugger limitation. Fields based on pointers that are contained in data structures cannot be displayed directly. To display their value, you need to EVAL the basing pointer and specify :X or :C followed by a length. In most cases, this step is not a big problem, but some variables cannot be displayed at all, which can make debugging more difficult.
The Dynamic Duo
After seeing how the dynamic data Mediator uses dynamically imported procedures and data to support I/O tasks, you will probably think of other areas that could benefit from this technique. Mediators can simplify your client applications and help you to create systems that are easily enhanced and modified. Writing a Mediator can be a complex task, but, by writing one, you are moving the complexity from your applications into a single area.
I hope you find Mediators as useful as I have. The ILE and RPG IV are what make Mediators practical. Mediators are a challenge to write, but when youre done, you may just the find time to take that vacation youve been putting off because your applications can be written much more quickly.
References
Design Patterns: Elements of Reusable Object-Oriented Software. Gamma, Erich, Richard Helm, Ralph Johnson, and John M. Vlissides. Reading, Massachusetts: Addison- Wesley Computer and Engineering Publishing Group, 1995
ILE C/C++ MI Library Reference V3R7 (SC09-2418-00, CD-ROM QBJADR00) ILE C for AS/400 Programmers Reference V4R2 (SC09-2514-00, CD-ROM QB3AG100)
System API Reference OS/400 Object APIs (SC41-5865-01, CD-ROM QB3AMQ01)
System API Reference OS/400 Program and CL Command APIs V4R2 (SC41- 5870-01, CD-ROM QB3AMV01)
LATEST COMMENTS
MC Press Online