Jim Karabatsos - GUI Computing
I know we shouldn't use them, but there are some times that you just absolutely need to keep a few things at a global scope in the application.
One category of global objects in an application are various database objects. I typically define a global database object called gDB (or for client-server work two database objects called gDBLOCAL and gDBREMOTE). This sort of global object is global because we don't want the overhead of opening and closing database objects repeatedly (not to mention server connections).
Another category of variables that are often defined at global scope are what I call application properties. These are things like Dirty or Busy flags, Status indicators or nesting levels. While we typically think of these as variables, they are really a little bit more than that - there are typically rules that specify how these variables are to be maintained and under what conditions they can be modified or even accessed.
In VB4, you could define a global object that exposed a set of properties that could be manipulated by the rest of the program; you could then encapsulate the rules surrounding access to and modification of those values in the property Let and Get procedures.
You can actually do something very similar in VB3. Other than global Database objects, as noted above, I will generally not define any other global variables. Instead, I have one or more modules in which I define variables of the appropriate types at module scope. Note that these are defined locally to the module - I do not use the Global keyword. I then define appropriate Get and Set procedures for each variable, something like this:
Dim mbDirty As Integer ' boolean Sub SetDirty(ByVal bValue As Integer) mbDirty = bValue <> 0 End Sub Function GetDirty() As Integer ' boolean GetDirty = mbDirty End If
Now, whenever I want to modify the Dirty flag, I simply use:
SetDirty True ' or False
Whenever I need to check the current state of the Dirty flag, I use the GetDirty() function.
OK, what's the point of doing this for something as simple as a global boolean? Well, even for something as simple as this, there are some rules.
The Dirty property is a Boolean property; however in VB3 we need to use an integer to hold a boolean value, zero representing False and anything else representing True. The problem with this scheme is what happens when we negate a boolean with VB's Not operator. The Not operator is a bitwise operator. Not zero equals -1 (which in binary - or more accurately twos-complement - consists entirely of 1 bits) and Not -1 equals zero. Unfortunately, applying a Not operation to any number other than -1 results in a different, non-zero number, which according to VB is still True.
Throughout our code, we need to watch out for subtle bugs that can arise from this sort of thing. For example:
If Len(SomeString$) Then ' the string is not empty End If
is quite valid, whereas:
If Not Len(SomeString$) Then ' the string is empty End If
will always think that the string is empty and should really have been coded:
If Len(SomeString$) = 0 Then ' the string is empty End If
Wouldn't it be nice if we could be sure that a Boolean variable could only hold zero or -1 (False or True)? Well, in VB4, we do indeed have a Boolean data type; unfortunately, this does not exist in VB3.
Using a programmatic layer between your application and the variables allows you to enforce these kinds of rules. Notice how the SetDirty procedure does not simply assign its parameter to module-level variable. Instead, it massages it so as to be either True (-1) or False (0).
Now the rest of the program can safely depend on the value returned from the GetDirty() function being a true Boolean value, and can safely use Not GetDirty() without any nasty surprises.
The point here is not that Booleans can be tricky. The point is that using this technique allows us to effortlessly validate or massage all values assigned to a global and that we can be sure that none of the code outside of that module can circumvent this validation.
I have used this technique to wrap up even complex logic. For example, let's say that there is a global variable to store a Status, and that there are five different values that it can have : Starting, Idle, Busy, Error and ShuttingDown. Further, let's say that there are some rules about what status can be set based on the previous status - for example, Starting cannot go directly to Busy, Error can only go to ShuttingDown and ShuttingDown can't be changed at all. If these status conditions are triggered by asynchronous events (say by messages arriving from another application or from a hardware device) then we need to implement these rules in many places throughout our application.
Now consider this:
Global Const STATUS_STARTING = 0 Global Const STATUS_IDLE = 1 Global Const STATUS_BUSY = 2 Global Const STATUS_ERROR = 3 Global Const STATUS_SHUTDOWN = 4 Const STATUS_LOW = STATUS_STARTING Const STATUS_HIGH = STATUS_SHUTDOWN Dim mnStatus As Integer Function GetStatus() As Integer GetStatus = mnStatus End Function Sub SetStatus(ByVal nValue As Integer) If (nValue < STATUS_LOW) Or (nValue > STATUS_HIGH) Then Exit Sub End If Select Case mnStatus Case STATUS_START If nValue <> STATUS_BUSY Then mnStatus = nValue End If Case STATUS_IDLE mnStatus = nValue Case STATUS_BUSY mnStatus = nValue Case STATUS_ERROR If nValue = STATUS_SHUTDOWN Then mnStatus = nValue End If Case STATUS_SHUTDOWN ' no changes allowed End Select End Sub
You can see how we have neatly expressed the rules in one place in an easy-to-maintain format. The rest of our application can now blindly try and set whatever status it thinks should apply and not care about these rules. When the current activity is finished, for example, a procedure might simply do this:
If there was no error condition or there was no request to shutdown, then this would indeed set the application status to Idle; however, if we had encountered an error condition (or had received a shutdown request) then there is no fear that this code would overwrite that value and violate our integrity controls.
As you can see, taking a little bit of time to encapsulate your global variables in Get and Set procedures actually significantly simplifies the rest of your program and is demonstrably more robust. What we are doing is using some OOP techniques in a non-OOP language. Even if your global variables do not need any logic, use this technique anyway. You'll thank me in due course, and much sooner than you might think.
Of course, if you are using VB4, you can use a class and its properties to do the same thing. VB4 allows you to make setting and reading a property value look more like accessing a variable directly, but otherwise does not really buy you anything new. Indeed, I actually prefer the explicit use of the Sub and Function pairs because (to my mind) it is more self-documenting.