Ken Bergmann
Microsoft Developer Network Technology Group
September 29, 1995
This technical article illustrates and discusses several fundamental coding practices and guidelines recommended for developing client/server solutions in Visual Basic. It is part of a series beginning with the article "Client/Server Solutions: The Architecture Process," also available in the Microsoft® Development Library.
The following are practices and techniques that should be used when writing Visual Basic code. Most of the items do not deal with style, but with proven techniques for writing solid code. The more habitual that good coding practices become for you, the easier it will be to read and modify your code. Standard techniques and practices also help in debugging and sharing code among programmers. Using these standards should provide some commonality among programming styles that otherwise might differ greatly.
You can modify these ideas to fit your individual coding style and the stability level of the project. Common sense should dictate which suggestions should be rules for code review and which are optional guidelines.
Table 1. Default Settings
Item | Comment |
Option Explicit | This is pretty standard. Always put this in every module and every form. |
Option Compare | String comparisons are handled differently depending on how this is set; therefore, always explicitly set this value in modules and forms. |
Option Base | This changes what the default lower bound of arrays is set to. Always explicitly set this value in modules and forms. |
DefVar | This will declare variables without type definitions to be a particular type. Always explicitly declare all variables to be of a certain type. Also define the defaults. |
Text | Always save all files as text. This will make available the many tools that utilize Visual Basic code stored as text. Examples are analyzers, source control systems, etc. Also in the case of reconstruction or reuse, text files are easier to manipulate than binary data files. |
Table 2. External Declarations
Item | Comment |
Declaration organization | External declarations should be grouped consistently and all should be included in the same module. Any grouping convention may be used as long as consistency is maintained. Some common groupings would be by library or by task type. |
Pointers to strings | Pointers to strings are common when using API functions. Because Visual Basic handles strings differently and because Visual Basic lacks pointers, you must handle these situations specially. Use the STRCPY API function to copy from a long pointer to a string. When copying a pointer to other data types, including user-defined data types (UDTs), use the MEMCPY API function. |
Table 3. Operations
Item | Comment |
Proper operator | Always use the proper operator for the data types involved. Always use the ampersand (&) when concatenating strings and the plus sign (+) when adding numeric values. If you use variants, it is possible to add two strings together and mistakenly produce a number instead of concatenating strings together. |
Explicit conversion | Always explicitly convert values to the appropriate data types before operating on them. This is essential when working with variants. The Val() function is a good general purpose number conversion routine. However, the standard CStr, CLng, and Ccur functions are considered the most proper of all forms. |
String comparisons | Using strings for comparisons is not recommended. If you must do this, however, I would suggest that you apply the UCase(Trim$()) formula to both sides of the comparison. This will ensure a case-insensitive, nonwhite-spaced (leading or trailing) comparison. |
Control usage | Never load list boxes, combo boxes, or grids with more than one hundred (100) static items. A good rule is that if there are more than fifty (50) items, use a virtual load or use a searchable system. One technique is to have a text box in which to enter the first few characters. Then when the focus leaves or the drop down is triggered, execute the search using the text as a filter and so limit the contents of the controls. |
Control loading | Most of the time needed to load controls is required by the paint events as data changes. Because of this, it is wise when loading controls, such as list boxes or grids, to either make the control invisible or—better still—turn off the redrawing of the control. Then, once the data is properly loaded, the control can be redrawn. This can be done easily with the SendMessage API function. |
Select Case | Using a Select Case instead of multiple If statements adds to readability and an increase in performance. Select Case statements are the preferred method for branching execution when there are more than three possible branches. Remember to cover all inputs. Always have an Else Case defined, and make sure that all criteria that can be handled are handled cleanly. Defined ranges are often helpful for covering all possible inputs. |
Resource usage | When using file handles or memory, always close the files and release memory. When using file handles, call FreeFile to reserve a handle. Always restore system property settings if you use them. |
Transaction usage | When using transactional logic or BeginTrans syntax, make sure that the logic begins and ends in the same function. For the BeginTrans syntax, this is required. For custom logic, it adds readability, consistency, and simplicity. |
Loops | Every loop should have well-defined, obtainable, termination criteria. This is especially true in algorithms that require recursion. |
Table 4. Variable Usage
Item | Comment |
Array declaration | You should always use the lower to upper syntax as in iArray(0 to 10). By explicitly defining the lower and upper bounds of static arrays, the impact of the Option Base statement can be removed. |
Array traversal | Because of the flexibility in array dimensioning and the availability of the Option Base statement, always use the LBound() and UBound() functions as starting and ending points when traversing an array from top to bottom. It is not recommended that you store the upper and lower bounds in variables. The LBound() and UBound() functions are the only ways to guarantee accuracy through ReDim statements. |
String literals | When using the same string literal more than three (3) times, you should implement the literal as a constant. This offers several benefits: compactness (constants require less memory for storage), efficiency (operations involving constants are much faster), and modularization (changes to the literal are confined to one place). |
Scoping technique | A general rule of thumb when deciding on the scope of a variable is: Give it as little scope as possible. If the variable is used in only one function, limit the scope to that function. If it is only used in one module, limit the scope to that module. Consider reorganizing code to follow these techniques while ensuring that your organization fits the requirements of the layered paradigm. |
Scoping initialization | Ensure that any variables that are not locally scoped are initialized before they are used. Variables that are of higher than local scope may be changed during the execution of a procedure. |
Table 5. Syntax and Structure
Item | Comment |
Variable declaration | Declare all variables on a separate line. Explicitly declare the type of each variable as you declare it. This enhances readability, offers easier debugging, and leads to fewer type definition errors. |
Executable statements | All executable statements should be on separate lines. While the Visual Basic processor accepts several statements separated by colons included on the same line, it is considered bad form. You should adopt readability as an important goal for the structure and syntax of your code; therefore, don't use shortcuts like this to simply conserve space. |
Goto statements | When writing structured code, avoid the use of Goto statements. There are special cases where the use of Goto statements can be considered acceptable and proper—for example, in error handling. However, you should limit your use of these statements to one or two labels per function and should uphold the guidelines on usability. |
Parentheses | Use parentheses in statements with care. For example, you should always use parentheses to force the order of evaluation when necessary. You might also use parentheses when you decide that the code would be more readable because of tricky precedence or to qualify information for operations. In Visual Basic, the use of parentheses introduces an extra step in the operation. A memory copy is performed, and the remainder of the operation is applied to the copy of the result of the operation within the procedure. You can take advantage of this copy functionality when writing critical code to ensure that operations don't change the value of a variable, but operate on a copy of the variable instead. Of course, this advantage comes at a slight expense in the performance of operations. |
Naming the main module | When naming the main module of a project, always give the primary module the same name as the executable name. This allows for cross-project consistency and for easier work transference. It establishes a common starting point for all applications. |
Table 6. Formatting
Item | Comment |
Consistency | Consistency is rule number one! |
Headers | Every module and every function in every module should have a header. Some information for the header is: Description, Inputs, Outputs, External Function Calls, External References, Revision history. Comments are very important for understanding process flow and other settings, especially during debug, and maintenance phases of application development. Inline comments are good for covering key areas of complexity. Header comments are used primarily to outline complex algorithms or routines. |
Error trapping | Every function should have an enabled error handler. In cases where failure is a simple exit, this lets you institute more complex error handling easily, with a minimum of changes. |
Naming conventions | It is considered proper when naming functions and controls to use a naming convention. Some recommended conventions are <object><verb> or <verb><object>. The choice is usually dictated by the vertical organization of the relevant code. For example: If several routines to retrieve data for several objects are included in the same module, I suggest you use <verb><object> naming such as: GetCustomer, GetProduct, GetOrders, and so forth. However if the routines to support a single object all reside in the same module, it is considered proper to use <object><verb> naming such as: CustomerLoad, CustomerUpdate, CustomerInsert, and so forth. |
Indentation | It's a good idea to indent your code to indicate the flow of control. Don't, however, indent the base level. The first level of code should be on the margin with no preceding tabs. Code that resides inside an If...Then...Else statement should be indented one tab. |
Table 7. Procedure Organization
Item | Comment |
Consistency | Consistency is rule number one! |
Defined inputs and outputs for functions | Every function should have a defined range for each input and a defined range for acceptable success and error return values. It is critical that the return ranges be defined, attainable, and nonoverlapping. |
Input value checking | Every procedure should perform range checking validation on all input values. This will often simplify algorithms and logic in the remainder of the routine. This practice will also allow you to catch and return error conditions as early as possible. By catching input range errors before commencing complex algorithms or logic, you can do cleanup and create graceful exit procedures with minimal logic impact. |
Parameter lists | Procedures that perform similar functions or tasks should have similar parameter lists and output values. This level of consistency lends itself readily to reusable and maintainable code. |
Modularize code | Code that is repeated or duplicated should be modularized. The general rule of thumb is that any code statement repeated more than three (3) times should be put in a separate routine. Similarly, code that exists in more than two (2) modules should be consolidated and generalized for reuse. |
Procedure depth | Procedures calling other procedures should not require too many levels of procedure nesting. A general rule of thumb is that the procedure depth should not exceed six (6) levels. |
Table 8. Writing Process
Item | Comment |
Consistency | Consistency is rule number one! |
Define coding goals | You should always have a defined list of objectives for each portion of code you write. Always reevaluate any code when you don't have a specific goal for its use. Never write code to provide a particular solution if the general case solution is as simple. |
Generalization | Before writing new code, always clearly think through the uses for the code and consider whether you can write it in a reusable fashion. Custom code is usually the easiest to write, but is almost never the easiest to maintain or reuse. |
Code reviews | Always review new code before releasing. Formal reviews are good, but not always practical. At a minimum, have all work read over by one of your peers to check for consistency and general appearance. Code that is poorly organized or isn't well commented can be spotted easily without intimate knowledge of the code. This quick once-over by another developer will serve as a good sanity check to prevent you from releasing substandard code to others. This is especially important in high stress situations with code that is quickly written. Never review more than two thousand (2000) lines of code at any one time: It's difficult to do a good job of reviewing such an unwieldy amount. |
Code analysis | Code that is being put into production should be run through analyzers or profilers. If it is a front-end application for a database system, it should also be thoroughly benchmarked. Analyzers can tell a lot about the design of code by generating statistics about the code, such as executable-to-comment ratios, lines per module, local-to-global variable usage, literal usage frequency, and module level function break downs. Analyzers should also be used to generate call trees and relationship diagrams. These are helpful for graphically understanding the organization and structure of the code. |
Code testing | Never test more than three (3) functions at a time. It is important for code coverage purposes that your testing be as focused as possible. By keeping the number of function jumps to a minimum, you can more effectively scrutinize the focus areas. |
These ideas are more suited to C++ developers, but with the changes in the Visual Basic language, developers using either language can benefit from these concepts.