Develop a Coupling Strategy

Nonspecified code can often suffer problems caused by tight coupling. Because nonspecified code doesn’t form part of a major unit, programmers often pay less attention to its interaction with other parts of the application. Where possible, you should pass parameters into procedures rather than use module-level variables. Input parameters that won’t need to be changed should always be declared by value (ByVal) rather than the default, by reference (ByRef). Many programmers choose the by reference method simply because it saves typing.

Another common excuse for using ByRef is the argument of speed: passing by reference is a few milliseconds faster than passing by value because Visual Basic has to create a copy of the variable when it’s passed by value. But the consequence of misusing ByRef can be severe in terms of debugging time. Imagine a seldom-used application configuration variable that gets inadvertently changed by another procedure. You might not detect the error until someone uses the configuration function several times, maybe even long after you’ve written the code. Now imagine trying to trace the cause of the problem! As a rule, always pass parameters by value unless you explicitly want changes to be passed back to the caller.

The purposes of passing parameters to a procedure rather than using module-level variables are to make it obvious to anyone not familiar with the code exactly what external dependencies are being used and to allow the procedure to be rewritten or reused more easily. A good practice is to document procedure parameters in a header box. A header box is simply a series of comments at the beginning of a procedure that explain the purpose of the procedure. Any external dependencies should also be documented here. Often programmers do not reuse functionality simply because the parameters or dependencies are unclear or not easy to understand. Imagine a procedure that accepts an array containing 20 data items. If the procedure is dependent on all 20 data items being present, other programmers might find it difficult to use unless it is well documented.

Passing parameters to procedures allows you to create code that is loosely coupled and therefore potentially reusable. The following code fragment shows a would-be reusable procedure that is too tightly coupled to the form it’s in to be reused anywhere else in that application, let alone in another application:

Sub SearchForFile(ByVal isFile As String)

    ' Disable all buttons.
    cmdClose.Enabled = False
    cmdView.Enabled = False

    ' Process
    §
    labStatus = "File " & isFile
    §

The procedure is rewritten here in a more reusable way:

Sub cmdProcess_Click()

    Dim ctlDisableArray(0 To 1) As Control
    Dim sFile                   As String

    ' sFile = filename
    ctlDisableArray(0) = cmdClose
    ctlDisableArray(1) = cmdView

    Call SearchForFile(sFile, ctlDisableArray(), labStatus)
    §
End Sub

Sub SearchForFile(ByVal isFile As String, _
    Optional ctlDisable() As Control, _
    Optional labUpdate As Label)

    Dim nIndex   As Integer

    ' Disable all buttons if any are specified.
    If Not IsMissing(ctlDisable) Then
        For nIndex = LBound(ctlDisable) To UBound(ctlDisable)
            ctlDisable(nIndex).Enabled = False
        Next nIndex
    End If

    ' Process
    §
    If Not IsMissing(labUpdate) Then
        labUpdate = "File " & isFile
    End If
    §

Now the procedure is totally decoupled and can be called from anywhere in the application.

Another good practice to adopt is using more flexible parameter types for inputs to a procedure. In Chapter 5, Jon Burn says, “Use variants for everything.” If you take Jon’s advice, you should be careful to validate the parameters and display helpful errors. In a simple application, you can easily locate the cause

of an error; but if the error occurs in a compiled ActiveX control, it might be a different story. The sample code here is the procedure declaration for a subroutine that fills a list box from an array:

Public Sub FillList(lst As ListBox, anArray() As Integer)

The function might work fine, but it’s restrictive. Imagine you have another type of list box control that has some added functionality. You won’t be able to pass it into this function. It’s also possible that someone might want to use this routine with a combo box. The code will be similar, so this is a feasible request. However, you won’t be able to use the procedure above with a combo box. If the routine is part of the application, you can rewrite it; more than likely, however, you’ll write another routine instead. If the routine is in a DLL file, rewriting it might not be so easy. In the following code, the procedure header is changed to make it more generic and the rest of the code is added as well:

Public Sub FillList(ctl As Control, anArray() As Integer)
    Dim nIndex  As Integer

    For nIndex = LBound(anArray) To UBound(anArray)
        ctl.AddItem anArray(nIndex)
    Next nIndex

End Sub

Notice the potential problem now in this routine, however. If any control that doesn’t have an AddItem method is passed to the routine, it will fail. It might be some time later, when another programmer calls the routine, that the error is detected; and if the routine is in a DLL, it might take some time to debug. What we need is some defensive programming. Always try to code as if the procedure is part of an external DLL in which other programmers cannot access the source code. In this example, you can use defensive coding in two ways: by using Debug.Assert or by raising an error.

The Debug.Assert method, new to Visual Basic 5, evaluates an expression that you supply and, if the expression is false, executes a break. C programmers use these assertions in their code all the time. This method is intended to trap development-type errors that you don’t expect to occur once the system is complete. You should never use assertions in a built executable; therefore, the method has been added to the Debug object. In a built executable, Debug.Assert is ignored, just as with the Debug.Print method. You could use an assertion here like this:

Public Sub FillList(ctl As Control, anArray() As Integer)

    Dim nIndex  As Integer

    ' Assert - This subroutine handles only ListBox and ComboBox.
    Debug.Assert TypeOf ctl Is ListBox Or _
        TypeOf Ctl Is ComboBox

    For nIndex = LBound(anArray) To UBound(anArray)
        §

This will now trap the error if the routine is running in design mode. Because the debugger will break on the assert line, it’s always best to put a comment around the assert so that another programmer triggering the assert can easily identify the problem.

With our example, the assert is not a good method to use for defensive programming because we might put this routine into a DLL, in which case the assert would be ignored and the user would get an error. A better way would be to raise an error. When you raise an error, the code that calls this function will have to deal with the problem. Think of the File Open procedure in Visual Basic. If you try to open a file that doesn’t exist, the Open procedure raises an error: “File not found.” We can do the same with our routine:

Public Sub FillList(ctl As Control, anArray() As Integer)

    Dim nIndex  As Integer
    
    Const ERR_INVALID_CONTROL = 3000
    
    If Not(TypeOf ctl Is ListBox) And _
        Not(TypeOf ctl Is ComboBox) Then
    
        Err.Number = ERR_INVALID_CONTROL
        Err.Description = "An invalid control " & ctl.Name & _
            " was passed to sub 'FillList' - "
        Err.Raise Err.Number

    End If

    For nIndex = LBound(anArray) To UBound(anArray)
        §

This method will work in any situation, but it has two problems. The first problem is not really a problem in this instance because the caller won’t be expecting an error. If the caller were anticipating an error, however, we might want to check the error number and perform a specific action. Visual Basic 4 allowed type libraries in which you could declare constants and declarations to include in a project. The main problem with these was that you couldn’t create a type library within Visual Basic. It also meant that any client project would need to include the type library, thus increasing dependencies.

In Visual Basic 5, you can use a new feature, Enum constants. Let’s see how the code looks before we explain what’s happening:

' General declarations

Public Enum CustomErrors
    ERR_INVALID_CONTROL = 3000
    ERR_ANOTHER_ERROR
    §
End Enum

Public Sub FillList(ctl As Control, anArray() As Integer)

    Dim nIndex  As Integer
    
    If Not(TypeOf ctl Is ListBox) And _
        Not(TypeOf ctl Is ComboBox) Then
    
        Err.Number = CustomErrors.ERR_INVALID_CONTROL
        Err.Description = "An invalid control " & ctl.Name & _
            " was passed to sub 'FillList' - " &
        §

The constants are declared between the Enum…End Enum, just as in a user-defined type. The Enum name can be used to explicitly scope to the correct constant if you have duplicates. Notice that the second constant in the example doesn’t have a value assigned. With enumerated constants, if you specify a value, it will be used. If you don’t specify a value, one is assigned, starting from 0 or 1 plus the previous constant. Enumerated constants can contain only long integers. The big advantage in using enumerated constants is that they can be public. For example, if you create a class, any client of that class can access the constants. Now you don’t have to have constants with global scope, and you don’t need to create type libraries. In effect, the module becomes more encapsulated.

The second potential problem with the function on page 670 is that the array might be empty—but not the kind of empty that you can check with the IsEmpty function. If our sample code were to be passed an array that didn’t contain any elements (for example, it might have been cleared using Erase), you would get a “Subscript out of range” error as soon as you used LBound on it. A much better way of passing arrays is to use a Variant array. A Variant array is simply a variable declared as type Variant that you ReDim. If the array has no elements, IsEmpty will return True. You can also check that an array as opposed to, say, a string has been passed. The code looks something like this:

Public Sub FillList(ctl As Control, vArray As Variant)

    Dim nIndex  As Integer
    
    ' Exit if array is empty.
    If IsEmpty(vArray) Then Exit Sub

    ' Exit if not an Integer array.
    If VarType(vArray) <> (vbArray Or vbInteger) Then
        ' Error

The techniques described all help you to achieve the following benefits: