[Previous] [Next]
One of the easiest ways to make your code more understandable is to give your procedures descriptive names. Function names such as DoIt , GetIt , and PrintIt aren't nearly as understandable as CalculateSalesTax , RetrieveUserID , and PrintSpreadsheet .
The first thing to remember when naming a procedure is that DOS is gone (not dead and gone, but for the most part gone). Many programmers from the DOS days are still trying to break old habits that hinder rather than benefit development. For instance, some languages commonly used in the past limited the number of characters you could use to define a procedure name . We still see programmers choosing procedure names such as CALCTAX , GETIT , GETREC , and DELREC . If you're doing this, it's time to make a change. Code consisting of abbreviated procedure names is difficult (at best) to understand and maintain, and there's simply no valid reason to do this anymore. Certainly, procedure names such as CalculateSalesTax , RetrieveEmployee , and DeleteEmployee are much more self-explanatory than the older names.
Correctly naming procedures can make all the difference in the world when it comes to debugging and maintaining a project. Take naming procedures seriously; don't reduce understandability for the sake of reduced typing.
Incorrect:
Private Function InvoicePost() As Boolean Private Function PrintIt() As Boolean Public Sub SaveItem() Public Sub DeleteRecord() |
Correct:
Private Function PostInvoice() As Boolean Private Function PrintSpreadsheet() As Boolean Public Sub SavePicture() Public Sub DeleteContact() |
3.1.1 Use mixed-case letters when defining procedure names. In the past, some languages limited you to uppercase procedure names. This limitation does not exist in Visual Basic, and you should define all procedure names in mixed-case letters. IT'S HARDER TO READ SENTENCES IN ALL UPPERCASE THAN IT IS TO READ THEM IN MIXED CASE, and the same holds true for procedure names. By the way, all-lowercase names are almost (but not quite) as difficult to read as all-uppercase names. For instance, compare EmployeeAge to EMPLOYEEAGE and employeeage . Also, it's OK to use an underscore in a variable name to simulate a space (for example, Employee_Age ), but if you do use underscores, use them consistently. Note that you cannot use spaces in variable names.
Incorrect:
Private Function POSTINVOICE() As Boolean Public Sub savepicture() |
Correct:
Private Function POSTINVOICE() As Boolean Public Sub savepicture() |
3.1.2 Don't abbreviate when defining procedure names. If you ask two people how to abbreviate a given word, you'll probably end up with two different answers. If you believe that specific items within your application should be abbreviated, document those situations and make sure everyone uses those abbreviations all the time. Definitely don't abbreviate certain words in some procedures but not in others.
Incorrect:
Private Function SaveEmp() As Boolean Public Sub DelContact() Public Sub CalcTax() |
Correct:
Private Function SaveEmployee() As Boolean Public Sub DeleteContact() Public Sub CalculateSalesTax() |
Debugging code in an event-driven application can be quite challenging ”procedures are called from within procedures (fan-out), and code execution often bounces around like a billiard ball on a bumper pool table. The simple statements Exit Function and Exit Sub can compound this complexity. Every procedure has a single entry point, and this makes sense. You wouldn't want different calling procedures to be able to enter a given procedure in different code locations ”that would be a nightmare. Although it's not as obvious, every procedure should also have a single exit point.
When you can be assured that control always exits a procedure in the same way, at the same line of code, debugging becomes considerably easier. If you allow a procedure to be exited from anywhere within the code, how can you be sure that cleanup code is always executed before the procedure is exited? Sure, you might be able to remember exactly what is occurring within a procedure, but what about when another developer edits the procedure to add just one more Select Case clause and uses an Exit Sub to exit the procedure? Any code that should be executed to clean up the procedure sits unused, and problems can easily result.
The solution is to create a label called PROC_EXIT in every procedure. Underneath this label, place all necessary cleanup code and an appropriate Exit statement ( Exit Sub , Exit Function , or Exit Property ). Whenever you need to exit the procedure, include a GoTo PROC_EXIT statement rather than directly invoking the Exit command.
Creating single exit points makes procedures more like black boxes. Execution comes in one door and leaves through another door, resulting in fewer errors and a less difficult debugging process. The act of creating single exit points is amazingly trivial, but the rewards are tremendous. It's not often that the rewards will be so much greater than the effort required, so make sure to take advantage of it!
Incorrect:
Public Function IsFormLoaded(strFormName As String ) As Boolean '* Purpose : Determine whether a specified form is loaded. '* Accepts : strFormName - the name of a form. '* Returns : True if the form is loaded, False if not. On Error GoTo PROC_ERR Dim intCounter As Integer '* Since referring to a form loads the form, the proper '* way to determine whether the form is loaded is to loop '* through the Forms collection, which contains only '* loaded forms. For intCounter = 0 To Forms.Count - 1 '* If the current form is the specified form, '* return True and get out. If Forms(intCounter).Name = strFormName Then IsFormLoaded = True Exit Function End If Next intCounter '* Form was not found; return False. IsFormLoaded = False PROC_ERR: MsgBox "basMain IsFormLoaded" & vbCrLf & "Error: " & _ Err.Number & vbCrLf & Err.Description Resume Next End Function |
Correct:
Public Function IsFormLoaded(strFormName As String ) As Boolean '* Purpose : Determine whether a specified form is loaded. '* Accepts : strFormName - the name of a form. '* Returns : True if the form is loaded, False if not. On Error GoTo PROC_ERR Dim intCounter As Integer '* Since referring to a form loads the form, the proper '* way to determine whether the form is loaded is to loop '* through the Forms collection, which contains only '* loaded forms. For intCounter = 0 To Forms.Count - 1 '* If the current form is the specified form, '* return True and get out. If Forms(intCounter).Name = strFormName Then IsFormLoaded = True GoTo PROC_EXIT End If Next intCounter '* Form was not found; return False. IsFormLoaded = False PROC_EXIT: Exit Function PROC_ERR: MsgBox "basMain IsFormLoaded" & vbCrLf & "Error: " & _ Err.Number & vbCrLf & Err.Description Resume Next End Function |
3.2.1 Create robust exit code. Don't get complacent when adding cleanup code to a procedure. If the proper exit process depends on the states of certain variables, check the values of those variables and respond accordingly in your cleanup code.
Incorrect:
Private Sub cboTerms_Requery() '* Purpose : Fill the Terms custom combo box. On Error GoTo PROC_ERR Dim strSQL As String Dim rstTerms As Recordset cboTerms.Clear strSQL = "SELECT [InvoiceTerm] FROM tblInvoiceTerms " & _ "ORDER BY [InvoiceTerm];" Set rstTerms = dbSales.OpenRecordset(strSQL, dbOpenForwardOnly) '*AddalloftheinvoicetermstotheTermscombobox. Do While Not rstTerms.EOF cboTerms.AddItem rstTerms![InvoiceTerm] rstTerms.MoveNext Loop PROC_EXIT: rstTerms.Close Set rstTerms = Nothing Exit Sub PROC_ERR: Call ShowError(Me.Name, "cboTerms_Requery", Err.Number, _ Err.Description) GoTo PROC_EXIT End Sub |
Correct:
Private Sub cboTerms_Requery() '* Purpose : Fill the Terms custom combo box. On Error GoTo PROC_ERR Dim strSQL As String Dim rstTerms As Recordset cboTerms.Clear strSQL = "SELECT [InvoiceTerm] FROM tblInvoiceTerms " & _ "ORDER BY [InvoiceTerm];" Set rstTerms = dbSales.OpenRecordset(strSQL, dbOpenForwardOnly) '*AddalloftheinvoicetermstotheTermscombobox. Do While Not rstTerms.EOF cboTerms.AddItem rstTerms![InvoiceTerm] rstTerms.MoveNext Loop PROC_EXIT: '* If a database error occurred, rstTerms won't be set to '* a valid recordset. If Not (rstTerms Is Nothing ) Then rstTerms.Close Set rstTerms = Nothing End If Exit Sub PROC_ERR: Call ShowError(Me.Name, "cboTerms_Requery", Err.Number, _ Err.Description) GoTo PROC_EXIT End Sub |
Scope refers to the visibility of a variable or procedure within a project. Procedures can be defined as having either module-level, global, or friend scope. When a procedure is declared with the Private keyword, it has module-level scope and can be called only from procedures within the same module. A procedure declared with the Public keyword has global scope and can be called from any module within the project. In addition, public procedures of public class modules are available to external programs. Declaring a procedure (in a public class module) with the Friend keyword makes the procedure public to all modules within the project, but it does not make the procedure public to external programs.
When creating a procedure, always explicitly define its scope. Although it's possible to define a procedure without using Public , Private , or Friend , you should avoid doing this. Take a look at the following procedure definition in a standard module. Is this procedure private to the module, or is it public to all modules within the project?
Sub DisplayConfirmationMessage() End Sub |
The previous procedure is actually a public procedure because Visual Basic uses Public scope as the default. If your intent really is to create a public procedure, make that clear to the reader by explicitly declaring the procedure with the Public keyword like this:
Public Sub DisplayConfirmationMessage() End Sub |
Often, procedures declared without Public , Private , or Friend are really intended to be module-level ( Private ) procedures. However, without being specifically declared as Private , they are inadvertently created as Public procedures. Reduce the amount of effort required of the reader by giving each and every procedure a clearly defined scope. Also make sure that you're giving a procedure the scope that makes the most sense. If a procedure is called by only one other procedure within the same module, make it Private . If the procedure is called from procedures in multiple modules, explicitly declare the procedure as Public .
3.3.1 Every procedure definition should begin with Public , Private , or Friend . If you have existing procedures without one of these keywords, go through your project to determine the proper scope for each procedure and modify their declarations accordingly.
Incorrect:
Sub CalculatePOTotals() End Sub |
Correct:
Public Sub CalculatePOTotals() End Sub |
Although they're not quite as problematic as global variables, module-level variables should be avoided as much as possible as well. In general, the smaller the scope of a variable the better. One way to reduce module-level and global variables is to pass data between procedures as parameters, rather than having the procedures share global or module-level variables.
3.4.1 Specify a data type for each and every parameter. This can't be stressed enough. When creating procedures with parameters, always explicitly declare each parameter as a specific data type. For more information on data types, see Chapter 6, "Variables." When you omit the As < type > portion of a parameter declaration, the parameter is created as a Variant. I'll discuss the problems inherent with the Variant data type in Chapter 6. If you want to create a Variant parameter, do so explicitly using As Variant.
Incorrect:
Private Sub CreateStockRecord(ItemID, Repair, Quantity) |
Correct:
Private Sub CreateStockRecord(strItemID As String , blnRepair _ As Boolean , sngQuantity As Single ) |
3.4.2 Pass data ByVal or ByRef , as appropriate. Visual Basic passes data to procedure parameters by reference unless told to do otherwise . When a variable is passed by reference ( ByRef ) to a parameter of a procedure, the procedure receives a pointer to the original variable. Any subsequent changes made to the parameter are made to the original variable. On the other hand, when a variable is passed by value ( ByVal ) to a parameter, the procedure receives a copy of the variable. (String variables are an exception to this rule.) Any changes made to the parameter are made only to the copy, and the original variable remains unchanged. One possible standard would be to preface each parameter with ByRef or ByVal . That way you're explicitly declaring which process to use, and the code is more self-documenting . However, the discipline required to preface every parameter with ByRef or ByVal is great.
Another, and definitely easier, solution is to preface a parameter with ByRef only when that parameter will be used to change the value of the original variable (also called the argument). In that case, this practical application would read, "When a parameter is used to change the value of its argument, explicitly declare the parameter as ByRef ." Then you can add a comment to the Accepts section of the procedure comment header stating that the parameter is being used as an output parameter ”that is, to change the value of its argument. This approach gives you the best of both worlds .
Incorrect:
Public Sub ParseName(strFullName As String , strFirstName As String , _ strMiddleInitial As String , strLastName As String ) |
Correct:
Public Sub ParseName(strFullName As String , _ ByRef strFirstName As String , _ ByRef strMiddleInitial As String , _ ByRef strLastName As String ) '* Purpose : Parse a full name such as "John Smith" or "John D. '* Smith" into the proper separate components. '* Accepts : strFullName - the name to parse. '* strFirstName, strMiddleInitial, and strLastName are '* used as output parameters. This procedure populates '* the corresponding arguments with the proper values. |
3.4.3 Always validate parameters ”never assume that you have good data. A common mistake among programmers is to write procedures that assume they have good data. This assumption isn't such a problem during initial development when you're writing the calling procedures. Chances are good then that you know what the allowable values for a parameter are and will provide them accordingly. However, the following situations can be troublesome if you don't validate parameter data:
Given these possibilities, and given the fact that it doesn't take much effort to validate incoming data, it's simply not worth the risks not to validate incoming data. Validating parameters is one of the first tasks you should perform in a procedure.
Incorrect:
Public Function DuplicateContact(lngContactNumber As Long ) As Boolean '* Purpose : Create a new contact that is a duplicate of '* the specified contact. '* Accepts : lngContactNumber - the unique ID of the contact to '* duplicate. '*Returns:Trueifsuccessful,Falseotherwise. On Error GoTo PROC_ERR End Function |
Correct:
Public Function DuplicateContact(lngContactNumber As Long ) As Boolean '* Purpose : Create a new contact that is a duplicate of '* the specified contact. '* Accepts : lngContactNumber - the unique ID of the contact to '* duplicate. '*Returns:Trueifsuccessful,Falseotherwise. On Error GoTo PROC_ERR '* Make sure a valid contact number has been specified. If lngContactNumber <= 0 Then GoTo PROC_EXIT PROC_EXIT: Exit Function End Function |
3.4.4 Use enumerations when a parameter accepts only a small set of values. Chapter 5, "Using Constants and Enumerations," describes enumerations in great detail, but they bear mentioning here also. If a parameter accepts only a small set of values, create an enumeration for the parameter. Using enumerations reduces the potential of data entry errors in coding. Remember, however, that declaring a parameter as an enumerated type does not ensure that a value passed to the parameter is a member of the enumeration; you must still validate the data. Enumerations are powerful and have many advantages ”whenever possible, consider using an enumeration.
Incorrect:
Public Sub PrintReport(strRptFileName As String , _ lngDestination As Long ) If lngDestination < 0 Or lngDestination > 1 Then GoTo PROC_EXIT PROC_EXIT: Exit Sub End Sub |
Correct:
Public Sub PrintReport(strRptFileName As String , _ lngDestination As tpsPrintDestination) '*Makesureavaliddestinationhasbeenspecified. If lngDestination <> tpsScreen And _ lngDestination <> tpsPrinter Then GoTo PROC_EXIT End If PROC_EXIT: Exit Sub End Sub |
Visual Basic offers numerous shortcuts you can take when writing code. Generally, these shortcuts don't affect performance, but they often do sacrifice the readability of your code in favor of saving a few keystrokes at development time. You should attempt to make your code as self-documenting as possible. One area in which you can take shortcuts, but shouldn't, is when calling procedures.
You can call a procedure in a number of ways. When calling a Sub procedure, you can use the word Call or leave it out. For instance, the following statements will both call the same Sub procedure:
Call ShowError("clsApplication", "ShowRep", _ Err.Number, Err.Description) ShowError "clsApplication", "ShowRep", Err.Number, Err.Description |
Although you can omit the word Call and save yourself having to type two parentheses to boot, you should avoid this technique. The Call keyword specifically indicates that the statement is calling a Sub procedure as opposed to a Function procedure, and it makes the code more easily readable.
Visual Basic lets you call a Sub procedure and a Function procedure in exactly the same way. Consider this function:
Public Function DisplayContact(lngContactNumber As Long) As Boolean End Function |
You can call this function by using any of these statements:
Call ShowRep(lngRepNumber) ShowRep lngRepNumber blnResult = ShowRep(lngRepNumber) |
To make code as self-documenting as possible, differentiate between calling Sub procedures and calling Function procedures. Always use the Call keyword when calling Sub procedures, and always retrieve the value of a Function call ”even if you aren't going to use the value. The first statement in the previous example is misleading; it looks like a Sub procedure call rather than a Function procedure call. Although the second statement is better than the first, it does not retrieve the result of the function. This is not nearly as legible as the third statement, which does retrieve the result. If you aren't going to use the result of a Function call, consider placing a comment in front of the Function call stating what is returned from the function and why you don't need it.
3.5.1 Always use the Call keyword when calling Sub procedures. By using Call when calling Sub procedures, it's easier to distinguish the call from a Function call.
Incorrect:
PerformWordMerge8 strMergeFile, strTemplateName |
Correct:
Call PerformWordMerge8(strMergeFile, strTemplateName) |
3.5.2 When setting a variable to the result of a function, include the parentheses in the Function call even if there are no arguments. When calling a Function procedure, it isn't necessary to use the parentheses if the procedure doesn't accept any arguments. However, the code becomes a little more self-documenting when you include the parentheses anyway. The parentheses make it easy to tell that the symbol is a function name as opposed to a variable name.
Incorrect:
strDefaultWareHouse = GetDefaultWarehouse |
Correct:
strDefaultWareHouse = GetDefaultWarehouse() |
3.5.3 Always retrieve the return value of a function, even if you aren't going to use it. Although Visual Basic lets you call a Function procedure without retrieving the function's return value, you should always retrieve the return value even if you aren't going to use it. This improves readability and can greatly aid the debugging process. Many functions return a result code, indicating whether the function was successful. Although you might not need to use the result code right away, it might become important later.
Incorrect:
Public Sub OpenDocument(strFileName As String ) '* Purpose : Launch the associated program of the specified '* document. '* Accepts : A full path and file name of a document. On Error GoTo PROC_ERR ShellExecutefrmMain.hwnd, "Open", strFileName, 0, 0, 1 PROC_EXIT: Exit Sub PROC_ERR: MsgBox Me.Name & " OpenDocument" & vbCrLf & "Error: " & _ Err.Number & vbCrLf & Err.Description Resume Next End Sub |
Correct:
Public Sub OpenDocument(strFileName As String ) '* Purpose : Launch the associated program of the specified '* document. '* Accepts : A full path and file name of a document. On Error GoTo PROC_ERR Dim lngResult As Long '* The ShellExecute function returns a value indicating the result '* of trying to open the document. We might need this value later. lngResult = ShellExecute(frmMain.hwnd, "Open", _ strFileName, 0, 0, 1) PROC_EXIT: Exit Sub PROC_ERR: MsgBox Me.Name & " OpenDocument" & vbCrLf & "Error: " & _ Err.Number & vbCrLf & Err.Description Resume Next End Sub |