MV to the web – Part 2
This is part 2 in the “MV to the web” series. In the first part I described code changes needed to specific functions to separate out the UI from the rules. In this part I’ll describe a structure you can use for all program item/subroutines to organize your modularized functions.
The intent here is to provide each BASIC subroutine with the resources it needs to process inbound requests. You don’t want your rule modules to be burdened with details about the environment, nor with client-specific details. In Part 1 of this series I discussed extraction of the UI from your code. The structure here allows you to call your UI-less, modularized code from somewhere else. In Part 3 I’ll discuss accessing your code from “somewhere else”.
Setting up the code environment
Modularized code cannot assume that the proper environment exists for it to execute. In the example in Part 1, we assume that the F.COUNTRIES variable has been opened to access a file. That internal subroutine can make that assumption if we ensure that the program item does a proper setup.
Here is a simple structure which all of your revised BASIC code might adopt to ensure proper setup, and to facilitate access from external clients:
SUBROUTINE WEB.SUBNAME
INCLUDE WEB.INIT
GOSUB INIT
IF ERR = "" THEN GOSUB MAIN
INCLUDE WEB.WRAPUP
RETURN
*
INIT: * do local initialization
...
RETURN
*
MAIN: * branch based on OPeration requested
BEGIN CASE
CASE OP="AAA"
GOSUB DO.AAA
CASE OP="BBB"
GOSUB DO.BBB
CASE 1
ERR="Invalid operation requested"
END CASE
RETURN
*
DO.AAA: *
...
RETURN
*
DO.BBB: *
...
RETURN
END
All of my code looks like that for all of my web projects. It simply works. Let’s break it apart.
Note that there are no arguments on the SUBROUTINE line. This is because all values are provided via Common.
The WEB.INIT item is included. It contains a definition for COMMON and other global variables used to track the state of the application. All programs use this to open frequently-used files, to identify the user, log access, etc. I’ll expand on this below.
After global initialization, a GOSUB is done to a local INIT routine that sets-up data used throughout this single program. It opens files that are not frequently used, or which may be used only in this program.
Assuming there are no errors in the INIT, the MAIN routine essentially says “why are we here?”. The OPeration is identified in WEB.INIT and passed here for processing. The MAIN routine can GOSUB to a local handler or it can CALL to an external process which has the exact same structure.
I’ve found it convenient to have a common WRAPUP function which logs the exit, checks ERR for subsequent notifications, etc. Even if you don’t feel you need a WRAPUP routine I’d advise you to code for it.
You might wonder about some details. There are any number of customizations and preferences which can be applied to that structure, and you are welcome to make different choices within this framework. For example:
What happens if the initialization fails?
A return statement in the INCLUDE item will cause a return back to the calling code, so you won’t fall through.
Why do subs have a “WEB.” prefix?
This is to ensure that anything called from the web is easily identifiable. You may have a CM01 routine to do customer maintenance, and WEB.CM01 is a convenient way to identify the web version of that. I put all remotely accessed programs in a file called WEB.BP. Why “WEB”? Without knowing the client all we can assume is that clients will be somewhere “out there”. You could use NET or GUI, but these may not be any more appropriate than WEB depending on the kind of work that you’re doing.
What if functionality is spread out amongst different programs?
Can these subs call to one another?
Sure, you just need to make sure that if some program returns an ERR value that you don’t continue processing. You should also try to avoid initialization overhead (see notes on WEB.INIT below). Your options and solutions here are really no different from other code that you’ve already been writing for years.
What if I need error messages in a different language?
My error handling is actually a bit more rigorous. I return a unique error ID, default text, and an error category. The UI can decide which it wants to use. For example, if the UI needs to change languages, it can use the ID to do a lookup in some language table. It can also make some logical choices based on the category – a data error might be echoed back to the user, like “record not found” but a more critical error like “file not found” should prompt application wrapup, notification of the admin, or other activity.
WEB.INIT
Because initialization functionality is localized in Included code, all you need to do is change this one item and the entire application will work by whatever rules you define. I do a lot in this routine, and I employ a trick that I haven’t seen elsewhere. Here is an example:
COMMON /WEB/ FILES(4,50), INITIALIZED, GLOBAL.STATE(50)
* FILES, 1 = file handles, 2=names, 3=current key, 4=current item
COMMON LOCAL.STATE(50)
INCLUDE WEB.EQUATES
ERR = ""
IF NOT(ASSIGNED(INITIALIZED)) OR NOT(INITIALIZED) THEN
GOSUB MAIN.INIT
IF ERR # "" THEN RETURN ; * avoids app code, returns to caller
LOG.DATA = "Initialized" ; GOSUB LOG
END
GOSUB PARSE.REQUEST
IF ERR # "" THEN RETURN
GOSUB SECURITY
IF ERR # "" THEN RETURN
IF 0 THEN
MAIN.INIT: *
INCLUDE WEB.FILE.OPEN
* setup other vars here
INITIALIZED = 1
RETURN
LOG: * common logging accessible to all programs
IF NOT(LOGGING) THEN RETURN
GOSUB GET.UNIQUE
WRITE LOG.DATA ON F.LOG,UNIQUE
RETURN
GET.UNIQUE: * returns ID unique to entire system
UNIQUE = SYSTEM(19) ; * change to suit preferences
RETURN
NOTIFY.ADMIN: *
CALL WEB.NOTIFY.ADMIN ; * local values passed to sub
RETURN
PARSE.REQUEST: *
* Get OPeration, user info, and other data from request data
* return ERR if request is invalid
RETURN
SECURITY: *
* return ERR if user shouldn't access specific operation
RETURN
END ; * terminates IF 0
Notice that there is code that gets executed all the time, some code gets executed only when the environment is not initialized, and some code is made available to any program that wants to use it.
WEB.EQUATES is simply an item to localize all global EQUATE statements for the application. You will probably have other EQUATE statements, inline in your code or perhaps file-specific blocks of EQUATES in separate items. This is no different than other code you’ve written.
The INITIALIZED flag is not assigned when first entering the environment on a new port. (Note that for Unidata it’s “IF UNASSIGNED(var)”) Since some environments or flavors will pre-assign values to zero, I cover with the extra check for NOT(INITIALIZED). Yes, you might encounter the situation where your attempt to initialize failed, and you don’t want to keep repeating your init code if it’s going to fail. Code around that as required.
The IF ERR # “” THEN RETURN doesn’t return to the code with the INCLUDE statement, it returns back to the code that called the code with the INCLUDE statement. This implies all code with this Included code Must be a subroutine. If that’s not the case in your environment, code accordingly.
Note how logging is centralized, easy to enhance, and easy to use for the entire application.
The WEB.FILE.OPEN code is Included because sometimes this code can get rather long, and it seems more elegant to remove it from this mainline code. It’s easier to maintain a single code item dedicated to file operations than to have to maintain such code in the middle of a more complex code set. Since we’re using Common, this could be done in a Call as well, thus reducing the size of every object module.
The “trick” is in that IF 0 THEN clause. Of course that condition will never be true, so nothing in the block will get accidentally executed as we fall through from the code above. Examples of what can be placed in these in-line subs include UniqueID generation, and notifications. In this case, each program knows that it can notify an admin with a local gosub. Programs don’t need to know how that notification is implemented – so you don’t need to change code throughout the app if you change from simple logging to email, or to SMS or voice calls.
COMMON
The details of the COMMON will change from one site to the next, but there are a few significant points that should be observed.
- You can manage file state any way you want, but file handles should be in common to avoid re-opening them on every execution.
- I use Named Common so that I don’t need to re-initialize common every time I come into this environment. However, I also use un-named common because I don’t want sessions from different users to conflict. Usage of Named/Un-named common does depend on the kind of connectivity you’re using, and on your DBMS and flavor settings.
- Many apps have several lines of COMMON with arrays and individual vars. Personally I prefer to use a single array for major categories (like one for files and one for all global values), then assign convenient names to all of the elements in WEB.EQUATES.
Keeping focus on GUI
You could use the above constructs in all of your code right now, and if you’re writing a new program I’d recommend it. But this is about making your code accessible from outside, and that’s what I’ll get into in Part 3.