Reuse Issues for the Programmer

Good computer programming relies on a structured methodology that consists of a number of individual disciplines. For example, commenting your code as you write it is a discipline that you must practice until it becomes second nature. It is a well-known fact that many programmers either do not comment their code at all or comment it only after it's been written. One of the reasons you should comment code as you write it is because at that time you know your exact thought processes and the reasons behind the decisions you're making. If you were to comment the code two weeks after writing it, you might still understand how it works, but you will probably have forgotten subtle details about why you coded something a certain way or what effects a line of code might have on other parts of the application. If you ever see lots of totally meaningless comments, you can be sure they were inserted after the code was written!

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:

  • You think the code is of poor quality.

  • The code doesn't meet your requirements.

  • You don't know the code exists.

  • The code is poorly documented or commented.

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.

Make Few Assumptions

In an application, you'll often need to use variables that contain indeterminate formats. For example, an application might have several variables or properties storing directory names. When using these variables, you have to add a filename to get a full file specification. How do you know whether the path stored in the variable contains a backslash? Most applications have an AppendSlash routine that adds a backslash to a path if it doesn't have one, so you might be tempted to assume the format just because you've run it once in debug mode to check. You need to keep in mind, especially in large projects, that values of variables are often changed by other programmers, so a path that has the trailing backslash in one instance might not in another. Depending on the application, you might discover these errors immediately or not until some later time. In the case of the backslash, rather than rely on it being there, assume it could be missing and use your AppendSlash routine to check for it everywhere. You should apply this thinking whenever a particular format cannot be guaranteed.

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 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 EnumEnd 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:

  • Create reusable and generic code by creating loosely coupled routines and components

  • Help others to reuse your code

  • Protect your code from errors caused by client code

Group Functionality

You might be surprised at how many applications contain functions that fall into a particular category but are fragmented across the application. Such fragmentation often occurs when many programmers are working on the same application and the module names don't clearly identify the type of functionality the modules contain. The ownership aspect also comes into play: some programmers don't like to amend another programmer's code. Access conflict might also be an issue. It is good practice to familiarize yourself with other modules in the application so that you can group functionality and also identify functionality that you can use. Grouping reusable functionality has the added benefit that it can be extracted in separate DLLs at a later stage.

Document Your Code

Visual Basic now has an excellent Object Browser. You can include detailed documentation about your functions that will prevent others from constantly having to ask questions about routines you have written. Unfortunately, for many programmers, writing documentation is like going to the dentist. It is also a task that never gets included in project plans, so it almost never gets done. It is vital that you increase your estimates to allow time for these tasks.

Refine Your Habits

We've made only a few suggestions here—there are lots more. If you decide on a set of good habits and constantly practice and refine them, your organization will benefit because of the development time you'll save, and you will benefit because you'll need to do much less debugging. Each time you complete a project, review it and identify areas that you could have improved. Reusability is an issue that has to be supported by the business, designed by the designers, and coded by the programmers. Unless all sides commit to the goal of reusability, you will be doomed to continually rewrite most of your code.


Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net