Perspective Shift: Thinking in APIs
Monolithic vs. API
There are countless programs and subroutines built for the telnet world. In some cases, these programs and subroutines can be thousands of lines long, many with multiple entry points and exit points. The great thing about this monolithic style is that once the program is running, it stays running until the program is finished. If the program needs to display a message, it can display a message whenever it needs. If the program needs to accept input, it can accept input at any time. When the program needs to calculate, it calculates. In the end, all the food groups of input, processing, and output are melded together into one big, monolithic meal.
Monolithic programming works great for telnet applications because once you're in the program, you stay in that program as long as it stays running and you have a solid telnet connection. For web-based applications however, there is no persistent connection and no way to keep one program running to do everything that needs to be done. For this type of programming, an API (application programming interface) may be a better approach.
With the API approach, subroutines are created for everything and each subroutine merely accepts input and produces output. API routines don't actually get the input from the user; they merely accept it. API routines don't actually display the output; they merely produce it for something else to display. A simple API routine might look something like figure 1. Notice that it does no INPUT of its own, no CRT nor PRINT; its job is simply to take the input that has been provided and produce the output. The caller of this routine can then capture input in whatever way it deems necessary and can display the output however it needs.
ANS = VALUE1 + VALUE2
API routines are not specific to web programming, however - they can be called both from telnet and non-telnet applications - as they have standardized inputs and outputs. Such a routine needs to know nothing about the environment in which it is called; it can take the input and produce the output completely unaware of its surroundings.
The problem with historical code is that the lines have been blurred between what a subroutine does and what a program does. Subroutines do more than simply convert input into output. Subroutines also prompt for input and produce output just like the programs that are calling them. In doing so, they are limiting their viability except in the interactive context in which they were first designed.
To get the most mileage out of subroutines, create API routines that avoid direct inputs and outputs. Subroutines should convert input to output and nothing more.
Separate Validation from Updating
When writing telnet-based screens, it's not uncommon for validation and updating to be intermingled into one coagulated mess. Start the validation, and if everything looks good, do some updates. Check a few more things, do a few more updates, rinse, repeat.
Of course, nobody we know would do this, right? Not only does this contribute to inconsistent updates, but it makes debugging harder (i.e. "How did the invoice get created without the G/L being updated?"). It prevents using the same routine in both a stateful and stateless environment.
Instead, we can write updates as API routines. Because these routines will focus on a single task — validation OR updates OR transformations — they are less likely to fail. However, if they do fail, they can fail gracefully, rolling back any changes that may have been made. Our separate API routines for validating will return information back to the caller about what is needed for the updates to be successful. The point is that by separating the parts, you can call a validation routine as many times as needed (and from any interface) and get a proper answer each time. Yet the saved information has not been affected because the updates are in a different routine. Only when everything is assured to update without incident should the update routine be called. For example, a simple validation API subroutine might look like Figure 2.
... extract variables from the record and read anything else that might
... be needed.
STATUS = 'ERROR'
IF (INV.DATE EQ '') THEN
IF (CUST.IS.ACTIVE) THEN
IF (CREDIT.LIMIT.IS.OKAY) THEN
STATUS = 'OK'
STATUS = 'NOLIMIT'
STATUS = 'INACTIVE'
STATUS = 'INVOICED'
Also, consider that "update" in this context may not actually involve a change to the database. It may simply involve transformation; changing a value in a memory variable. Makes no difference, all validation should be done before any updates occur.
Lock with Care
One of the concerns with API validation like this is that at some point a record needs to be locked to be updated. Is locking a part of the validation, or is it a part of the update? The answer is, of course, "YES." In other words, locking records for update is a function of both validation and updating.
In an API approach, the validation should be responsible for locking everything that needs to be updated. It should also be in charge of unlocking when everything needing to be locked can't actually be locked. This prevents traffic jams and deadly embraces. A record that cannot be locked is unable to be updated, and that should be as effective as any other error in stopping the update. This also implies that the validation API routine needs to communicate all those locked and loaded records to an update API routine somehow. The important part is that all of the validation is done first, and once the update is assured, the update can proceed.
In a telnet environment, we don't even think about managing state as we can hold all our variables in memory for as long as we need (or until a road crew "inadvertently" cuts our network connection). In a web environment, we can worry less about the road crew as there's no persistent connection to be lost. However, managing state is much more important in a web context to keep a conversation going between the user experience and the back-end processing.
So who is responsible for managing state? The short answer is that it shouldn't matter. When constructing applications using the API method, the same routines can be called from any number of interfaces, both stateful and stateless. As long as each routine receives the input it needs, it can produce the output that the caller requires. With these details out of the way, managing state becomes merely a detail in the process of constructing interfaces to the API routines.
Leverage the Adapter Design Pattern
Once the API routines are all in place, you can "adapt" the inputs and outputs from various contexts as needed. One adapter (calling routine) might be for the telnet application whereas another supports web, and yet another might be for a service architecture using a drop-file interface. The point is that the interface itself doesn't matter until you create the adapter.
You can create as many different interfaces into the application as needed, all using the compendium of API routines previously implemented. In the end, the adapter will hold the unique part of each user experience. All that logic in the API routines will be leveraged by each interface by flowing through the adapter. The rest of the application — daresay the majority of the code as implemented into API subroutines — can be reused by all of the interfaces.
There are several other issues to keep in mind when creating a "write once, use everywhere" application, but hopefully this establishes a foundation on which to begin the discussion.