Client/server architecture is a great idea for designing new applications, but what good is it for existing legacy systems? Joe Pluta guides you through the steps for converting a simple but functional legacy program into a client/server application. More than mere theory, this is actual hands-on modification of a working program. (All code used in these examples is available on the Web.)
What is a monolith? Merriam-Webster defines it as 1) a single great stone often in the form of an obelisk or column, 2) a massive structure, or 3) an organized whole that acts as a single unified powerful or influential force.
The first definition is what Arthur C. Clarke had in mind in his screenplay, 2001: A Space Odyssey (hey, Arthur, we’re almost there!), but the other two definitions better suit this discussion. When much of the legacy software we use today was written, there seemed little use for the CALL instruction. In fact, most programs were single monolithic entities, massive and powerful, although we could argue whether or not they were
“organized.” A normal item master maintenance program alone was about 4,000 lines of RPG code, not counting the display file DDS source.
You Say That like It Was a Bad Thing
Well, it was, especially if you wanted to modify your application. These programs did everything from business rules to screen formatting. It’s no wonder they were large and cumbersome. Add to that RPG’s global variables, and it’s easy to see how program modification backlogs continued (and still continue) to grow.
The other major issue with monolithic design came as installations grew larger and more diverse. Different sites and different users within a single site had different requirements. Some users needed to update certain fields, while others couldn’t.
One solution was to add more code to the already unwieldy maintenance program, introducing errors into what was a previously stable program. The other solution was to clone the program and change the clone to suit the new requirements. Unfortunately, that meant having two or more programs executing the same business rules. As business requirements changed, it meant changing all those cloned programs. In SSA’s BPCS, most of the Material Requirements Planning (MRP) inquiries, reports, and processing programs started with virtually the same processing code. However, they immediately began to diverge. Soon, although you could still recognize that the programs had the same origins, they could no longer be called clones. A fix for one program could not be applied to another without thorough analysis.
So How Do We Fix It?
This has been a goal of distributed programming from the beginning. “Client/server” often implies PCs talking to mainframes, but I can create a perfectly good client/server application where both client and server run on the same machine. Why would I do that? For one thing, to separate the user interface from business logic.
By creating one program that communicates with the user and another that accesses the database, I can change one without affecting the other. User interface programs tend to be small because they do very little work. However, application changes often involve changing only how the user communicates with software and not how the software actually functions. You don’t want to change the way your business works every time you turn around, even though your users can dream up new bells and whistles every day. By separating user interface from business rules, you can provide changes of convenience for your users without impacting day-to-day business functions.
For example, let’s say the shipping department needs to change the shipping addresses of its customers without seeing their account information, while the accounting department needs access to their credit limits. Business rules on maintaining fields don’t change from user to user. In a distributed program environment, we would simply clone Accounting’s user interface, remove the sensitive fields, and give the new copy to Shipping. Because both user interface programs call the same editing program, all business rules would be enforced, regardless of which interface was used.
Client/Server vs. Server/Client
You can use two basic strategies to reach this goal. To illustrate the difference, I need to introduce some terms that identify which portion of the architecture has actual control over program flow. For these examples, I’m assuming that the original monolithic program is broken in two: the user interface portion and business rules portion. As I discuss the two types of architecture, I’ll address the user interface portion first and then the business rules.
In client/server architecture, the user interface is the client. The client makes requests from the business rules processor, which is the server. For example, instead of reading a record from the database, it sends a GET request to the server, and, instead of editing the user’s data, it lets the server do the editing.
However, the client must also interpret user requirements. When the user wants to add a record, the client program first displays a blank input screen. The user enters data, and the client sends an ADD request to the server. If the user wants to copy a record, however, the client program first sends a GET request to get the original record and
displays the data to the user. The user then enters new data, and the client again sends an ADD request. If there are errors in either case, the client receives them from the server and displays them to the user.
The other strategy is server/client architecture. Here, the user requests a program, such as item maintenance. The user interface submits a call to that program, which typically runs in batch and perhaps on another machine. The program that runs in batch is almost exactly like the original monolithic program, except that instead of talking directly to a screen, it sends requests to the user interface. For example, wherever the program would originally do an EXFMT to a screen, it calls a program that sends the data fields to the user interface. The user interface then displays the data to the user, waits for input, and sends the user data back to the caller. In effect, the user interface is a display server, responding to requests from the “client” program running in batch.
Each approach has its benefits. If your goal is simply to change the mechanics of the user interface, server/client architecture is easier to implement. Servlet technology is one of the most talked about approaches today; the original programs simply speak HTML instead of 5250. (This is a bit oversimplified but close enough for our purposes.)
On the other hand, the client/server approach is the first step toward a truly object- oriented environment. Any program can use the server that is used by the item maintenance client. You begin to encapsulate access to your database within single programs. In a distributed environment, controlling access to your database is vital.
Having introduced the two architectures, I’ll immediately discard one. Server/client architecture has its uses but requires no real architectural savvy. Replace screen I/O op codes with calls to a program that sends a message containing the screen data and write a program that accepts those messages and displays them on the screen. When the user hits Enter, do the same process in reverse. This can be automated: Programs have been written that can split an existing monolithic RPG program into user interface server and business processing client.
In contrast, splitting a program into user interface client and business rules server takes more effort but is ultimately far more powerful. In this article, I do just that for a very simple master file maintenance program.
The Original Program
For this example, I wrote a simple maintenance application that maintains a simple item master file. The item master file has five fields: item number, description, quantity on hand, inventory unit of measure (UOM), and selling UOM. The application has two other files as well: the UOM master file and UOM cross-reference file, which contains conversions (for example, pounds to ounces). My business rules are simple, too:
• Duplicate item numbers cannot exist.
• Descriptions cannot be blank.
• Quantities cannot be negative.
• UOMs must exist.
• Inventory UOM and selling UOM must have a valid cross-reference.
I’ve written the original, monolithic program ITMMNT to be called by another program, such as an item list panel. ITMMNT takes an op code, item number, and return code. The structure is very straightforward:
• Set the process flag to blank.
• Based on the op code, execute subroutine ADD, MODIFY, COPY, DELETE, or
• Set the mode and display indicators, protecting fields as needed, for each subroutine.
• For the VIEW subroutine:
• Get the item.
• If the item is not found, set the process flag to error.
• If there is no error, EXFMT the screen and set the process flag to success.
• For all other subroutines:
• If not ADD, get the item.
• If the item is not found, set the process flag to error.
• While the process flag is blank, execute the PROCESS
• EXFMT the screen (here, the user can press F3 or F12 to exit, Enter to edit, or F6 to edit and update)
• If not user exit (F3 or F12), execute the EDIT subroutine:
• Clear errors.
• Test each condition— if there is an error:
• Set the field error flag on.
• Send message.
• Set the process flag to error.
• If there are no errors and the user requested update via F6, execute the update subroutine:
• For ADD or COPY, write a new record.
• For MODIFY, update existing record.
• For DELETE, delete existing record.
• Set the process flag to success.
• Endwhile The design is simple yet can maintain just about any master file where all fields fit onto a single screen. Errors are handled via a message subfile, which can display multiple errors. I also wrote a front-end program that prompts for an item number and performs an operation based on the command key pressed (see Figure 1).
For example, by pressing Enter, I get the View mode shown in Figure 2. If I press F10 instead, I get Modify mode, as in Figure 3. In Modify mode, the non-key screen fields are now unprotected, so I can change them. I can press Enter to edit my changes or F6 to edit and post them, assuming there are no errors. Errors cause a screen like the one in Figure 4 to display, showing invalid fields in reverse image and displaying error messages at the bottom of the screen. Pressing F3 or F12 exits the screen.
Splitting the Program
In the client/server model, I need a client that performs user interface and a server that performs database access. Let’s see how that split is accomplished.
The client changes very little. After removing all database files, the first 100 or so lines are virtually identical. I add data structures to support the middleware and the initialization and exit calls. The basic program structure remains the same.
The program doesn’t stay exactly the same, but the changes aren’t extensive. For example, the GETITM routine, which attempts to retrieve the requested item, is modified to request the item from the server rather than access the database directly. Figure 5 shows the changes. The CHAIN is replaced with two program calls: one to send the request and another to receive the response.
The other primary change comes in error handling. In the original program, the PROCESS subroutine first calls an EDIT subroutine and then an UPDATE subroutine (assuming no errors). The EDIT subroutine checks each business rule and, if one is broken, sends an error message. In the client/server model, there is no EDIT subroutine. The data is sent instead to the server with two additional pieces of information: the operation required (ADD, UPDATE, DELETE) and whether it is edit-only or edit-and- update. The server checks every field and returns any error messages. The client simply relays those errors to the user. See Figure 6 for an example of the changes. (With edit- and-update, the server also performs the requested master file I/O if there are no errors.)
To create the server, I just copy my EDIT and UPDATE subroutines and do a little rearranging. In the original program, the same database routines are called for every mode, so the routines check the mode indicators to determine what to do. The server, however, has a slight advantage because it is passed an op code right away, telling it whether to get, add, modify, or delete a record.
The program is highly modular. There are four primary routines: GET, ADD, MODIFY, and DELETE. GET is sort of on its own; it attempts to retrieve the record and either returns the record with success or returns an error if the record is not found. The other routines share some subsidiary routines:
• Execute EDTKY1 (make sure the record doesn’t exist).
• Execute EDTDTA (process non-key business rules).
• If there are no errors, write record.
• Return result.
• Execute EDTKY2 (make sure the record does exist).
• Execute EDTDTA (process non-key business rules).
• If there are no errors, get record and update it.
• Return result.
• Execute EDTKY2 (make sure the record does exist).
• If there are no errors, delete record.
• Return result. In all cases, each error causes an error message to be sent to the client using the DQMSSND middleware program (see below) rather than to a message subfile. After all editing is done, a final result record is sent reflecting the overall status of the request.
Middleware transports requests from client to server and responses from server back to client. In my example, a single message structure is used for all messages and all programs use a common error message structure.
Clients execute initialization once at the beginning of the program to establish a session and exit at the end to shut down the session. In between, they make as many requests as they need by executing the Client Send program to send data to a server and Client Receive to receive responses.
Servers receive the request message as their entry parameter. They send their responses back to the client with the Server Send program. Errors are sent one per message, using the predefined error message structure.
In our simple application, a message has eight fields:
• The session ID is assigned to the client during initialization.
• The server ID identifies the request target. (There is only one server in our case, but a client may very well talk to many servers in a single session.)
• The segment code controls multiple-message requests or responses.
• The operation code identifies the operation requested.
• The operation subcode modifies the operation requested.
• The return code indicates the success or failure of the request.
• The return subcode extends the return code when necessary.
• The message data contains the actual data specific to the request. (For example, for master file maintenance, it contains the record image of the master file.)
The error structure is a special structure designed to return field-specific error information to the client. If the return code indicates an error on a file maintenance request, the message data contains the error structure. Multiple errors can be received, one per message. The error structure has only three fields: field number, message ID, and message data. The field number is the index of the field causing the error. Note that the client and server must agree on which field these numbers represent.
Our application uses five simple programs as its API (all programs and fields in the API start with DQM, which stands for Data Queue Messaging):
• DQMCINIT is called at the beginning of a session to create a session ID (and response data queue).
• DQMCSND sends a message from the client to the server.
• DQMCRCV receives a response from the server.
• DQMCEXIT is called at the end of the session to delete the data queue.
• DQMSSND sends a response from the server to the client. Although this seems excessive, all five programs (and two subprograms) total less than 100 lines of code.
The Screens Remain the Same
You may have noticed that I don’t have a figure showing screens for the client/server version of the program. There’s a good reason for that: Those screens are exactly the same as the monolithic screens. Externally, the programs are exactly the same. However, we now have a client that can easily be cloned and modified and a server that can be called to do file updates while adhering to business rules.
We went from 229 lines of code for the monolithic program to 402 lines for the client and server, with another 90 or so lines of middleware. This may seem like a big increase for no apparent gain, but that’s not really the case. Those roughly 200 additional lines of code required for the client and server are pretty much all that will ever need to be added to make any program into client/server. Programs that are more complex will not require that much more additional coding, and you still get all the benefits of a truly distributed client. To see one of the benefits, read “The Business of Java” elsewhere in this issue, where we redeploy the client in Java on the workstation.
Where Do We Go from Here?
Technology is racing ahead. Those who live on the bleeding edge are changing their methodologies quicker than their socks. Last week’s solution was applets; this week, it’s servlets. We’ve gone from Write Once, Run Anywhere (WORA) clients to rewriting your entire legacy application in Java (not to mention replacing all those green-screen terminals with PCs). RPG programmers are nervous; IS managers are confused; and users are sure they aren’t getting all the benefits they should be. What’s an IS department to do?
My idea is to leverage everything we have and slowly move forward. We break the monolith into small, manageable pieces of code. We provide both green-screen and GUI clients while using the bulk of our existing legacy code. RPG programmers work on servers while Java programmers redevelop our interfaces on the workstation. And this is any workstation, from Macintoshes to LINUX machines. Cloned code is eliminated while functions are enhanced, and the workstation wizards build applications we never even dreamed of, combining images and sound with the good old business data we’ve grown to depend on. That business data comes from the programmers who’ve been with us the longest. Remember, most, if not all, of our application knowledge is in the heads of our legacy programmers; this is an irreplaceable resource. Any methodology that makes them obsolete is a recipe for disaster.
Object technology? We’ll get there. There’s a perfectly acceptable interim technique called “wrappering” that encapsulates existing legacy code into objects that can then be used by technologies such as Enterprise JavaBeans. The first step? Separating the user interface from the business processing logic, just as I’ve done in this article.
Don’t let the snake-oil salesmen tell you to get rid of your legacy code. They’ve tried that several times already. Today’s magic elixir is Java and JDBC, WORA, and EJB. Tomorrow’s may be something different. Don’t predicate your future on the latest quick- fix technology. Remember, there is no silver bullet, and there is no quick answer. Success is a journey, not a destination, and one we can all travel together.
Figure 1: The Item Selection program allows the user to enter an item number and select the action to perform.
Figure 2: In View mode, all entry fields are protected.