The discipline of coding for reusability is very important and comes only with practice. You will know when you've mastered this habit because you'll start writing less code. You should view any piece of code you write as a potentially reusable component. The experience gained from adopting this practice will help you not only to identify reusable units but also to anticipate the situations in which those units might be used. It will also enable you to make better decisions about how loosely or tightly the code can be coupled—it's not possible or efficient in all cases to decouple a code section completely from the other parts of the application. You should also remember that in a multiple-programmer project, other programmers will look to reuse code that other team members have written. Imagine you want a function and that function already exists: will you write it again or use the existing one? Obviously, you will reuse the existing function unless one or all of these conditions are true:
Experience will also help you make the right choices about the way that a unit couples to other units. A good practice to adopt is to write all your code modularly, encapsulating it as much as possible. A typical program consists of a series of calls to functions and subroutines. At the top level—for example, in a text box KeyPress event—a series of calls can be made. The functions that you call from within this event should, wherever possible, be coded as if they were contained in object components; that is, they should have no knowledge of the environment. It is the linking code, or the code in the KeyPress event, that needs to know about the environment. By coding functions and subroutines in a modular way, you can reuse them in a number of situations. You should also avoid embedding application-specific functionality in these top-level events because this prevents the code from being reused effectively. Look at the following sample code, which capitalizes the first letter of each word in the text box Text1:
Sub Text1_KeyPress(KeyAscii As Integer) If Text1.SelStart = 0 Then ' This is the first character, so change to uppercase. KeyAscii = Asc(UCase$(Chr$(KeyAscii))) Else ' If the previous character is a space, capitalize ' the current character. If Mid$(Text1, Text1.SelStart, 1) = Space$(1) Then KeyAscii = Asc(UCase$(Chr$(KeyAscii))) End If End If End Sub
The functionality in the KeyPress event is tied explicitly to Text1. To reuse this code, you would have to cut and paste it and then change every reference made to Text1 to the new control. The code would be truly reusable if written like this:
Sub Text1_KeyPress(KeyAscii As Integer) KeyAscii = nConvertToCaps(Text1, KeyAscii) End Sub Function nConvertToCaps(ByVal ctl As Control, _ ByRef nChar As Integer) As Integer If ctl.SelStart = 0 Then ' This is the first character, so change to uppercase. nChar = Asc(UCase$(Chr$(nChar))) Else ' If the previous character is a space, capitalize ' the current character. If Mid$(ctl, ctl.SelStart, 1) = Space$(1) Then nChar = Asc(UCase$(Chr$(nChar))) End If End If nConvertToCaps = nChar End Function
The nConvertToCaps function has no knowledge of the control it is acting on and therefore can be used by any code that has appropriate input parameters. You will often write procedures that you might not foresee anyone else using. By assuming the opposite, that all your code will be reused, you will reduce the time you or others require to modify functionality later for reuse.
The effects of not writing for reuse can be seen in many development projects but might not be obvious at first. At a high level, it is easy to break down an application into distinct components and code those components as discrete modular units using any of the methods described above. However, there is nearly always a large expanse of code that doesn't fit neatly into a distinct modular pattern. This is usually the application's binding code—that is, the logic that controls program flow and links various system components. Processes that are not major components in themselves but simply provide auxiliary functionality are normally assumed rather than specified formally, which is yet another reason why estimating can go wrong when this functionality is not considered. The result of bad design of these elements will usually lead to spaghetti code. The following sections discuss some habits that you should practice until they become automatic.
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 4, Jon Burn says, "Using Variants Instead of Simple Data Types" 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(ByVal 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(ByVal 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, introduced in 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(ByVal 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 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(ByVal 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.
Enumerated constants is a feature introduced in Visual Basic 5. 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(ByVal 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 the previous constant plus 1. 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 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 _ VarType(vArray) <> vbInteger Then ' Error
The techniques described all help you to achieve the following benefits: