Client/Server Solutions: The Basics

Ken Bergmann
Microsoft Developer Network Technology Group

September 29, 1995

Abstract

This technical article illustrates and discusses several fundamental concepts you should apply when developing client/server solutions using Microsoft® Visual Basic®. It is part of a series beginning with the article "Client/Server Solutions: The Architecture Process," which is also available in the MSDN Library.

Overview

This article will discuss several basic concepts tailored to help you design high-quality Microsoft® Visual Basic® code. Although these concepts are useful for programming in many languages, I've adapted them to meet the specific limitations and use the strengths of Visual Basic. These basic concepts are:

I've incorporated several examples throughout the following sections, but not every section has an example. For all sections, I suggest that you apply these ideas to existing code as a means of cross-examining the design that went into the code.

Consistency, Consistency, Consistency!

This should always be first and foremost in the thinking that goes into any design. As with most great concepts, this one is painfully obvious, but it is not practiced nearly enough. One reason for this is that different parts of code are often viewed from different perspectives by the same person. In the following example, a good naming convention is used for the variables, but the procedures have an obscure convention that isn't in line with the consistency of the variable names.

Dim iCustId As Integer
Dim sCustName As String
Global giProdId As Integer
Global gsProdName As String

Sub SaveCustomer()
Function RetrieveCustomerName()
Function UpdateAProduct()
Sub GetListOfProducts()

This same idea should apply to how we view controls, objects, stored procedures, tables, queries, and so on.

Another big advantage to consistency is the ability to use code generators much more effectively. Even code that performs different tasks using the same approach can easily be cloned; doing a simple search-and-replace is more advantageous than rewriting your code from scratch.

Simple First, Then Elegant

As the name implies, this concept is simple. You should write code first as simply as possible. When you do this, you can more easily see the limits and boundaries of the code and any errors present in the algorithm. Then later, as you polish the application and take advantage of performance improvements, you can modify individual functions with minimal impact, replacing simple routines with more elegant solutions.

There is a trap inherent in this concept that developers often get caught up in. Developers write simple functions with limited error handling, bounds checking, and so on, with the intention of returning at some later time to enhance the code and make it more robust. But as the project picks up speed, your "simple" code gets further and further from your mind. Finally, the project ships and you've never made time to go back through and clean up your code.

This type of coding is not what is suggested here. The form of the code should be intact and error handling should always be a priority. But it is possible to use simpler algorithms—perhaps slower or more memory-intensive—to manipulate data. For example, you might first use a bubble sort because the code is compatible and easy to reuse. Later, when you have more time to work on performance, you can substitute a quick-sort routine. Or you might use some simple, slow paint algorithm and then later optimize it using the newest flashy paint algorithm if necessary.

Modular Is Better

The whole idea behind modularity is to organize code consistently. This concept supports the Layered Paradigm. No matter what architectural approach you use, your code should always be modularly organized. The main benefit is that your transactional logic will be clear and concise. Tasks that are similar or that are performed by several applications are easily navigated and maintained because they share the same organizational structure and transactional flow.

Too Much Modularization Is Bad

When organizing code, it is important not to overmodularize. Consider the following arrangement:

Sub FillFocusCustomerArray(ByVal iCustId As Integer)
...
    sCustName = GetCustomerName(iCustId)
    sCustPhone = GetCustomerPhone(iCustId)
...
End Sub

Function GetCustomerName(ByVal iCustId As Integer) As String
...
    For iIndex = LBound(sCust,1) To UBound(sCust,1)
        If Val(sCust(iIndex,0)) = iCustId Then
            GetCustomerName = sCust(iIndex, 1)
        End If
    Next iIndex ...
End Function

Function GetCustomerPhone(ByVal iCustId As Integer) As String
    ...
    For iIndex = LBound(sCust,1) To UBound(sCust,1)
        If Val(sCust(iIndex,0)) = iCustId Then
            GetCustomerPhone = sCust(iIndex, 6)
        End If
    Next iIndex ...
End Function

This is a simple example drawn from real project source code. The justification for writing the code this way was that when the name or phone number for a customer was needed elsewhere, it could be found simply by using the customer ID. However, because the code is so modularized, it is difficult to follow the flow of control. Excessive modularization also slows down the code and creates more code to be maintained. This follows the "modular is better" idea perfectly, but it doesn't enforce the "simple first, then elegant" concept. Consider a simpler implementation:

Sub FillCustFocus (ByVal iCustId As Integer)
.......
    For iIndex = LBound(sCust,1) To UBound(sCust,1)
        If Val(sCust(iIndex,0)) = iCustId Then
            sCustName = sCust(iIndex, 1)
            sCustPhone = sCust(iIndex, 6)
        End If
    Next iIndex
    ...
End Sub

This approach is simple and modularizes the task. However, it doesn't have the previous code's advantage of being able to retrieve an arbitrary name or phone number simply by using the customer ID. Once again, you can make a compromise to provide maximum flexibility with quality code. By changing the requirement a little, you can supply similar functionality. Consider the following approach:

Sub FillCustFocus (ByVal iCustId As Integer)
    ...
    For iIndex = LBound(sCust,1) To UBound(sCust,1)
        If Val(sCust(iIndex,0)) = iCustId Then
            sCustName = GetCustomerName(iIndex)
            sCustPhone = GetCustomerPhone(iIndex)
        End If
    Next iIndex
    ...
End Sub

Function GetCustomerName(ByVal iIndex As Integer) As String
    ...
    GetCustomerName = sCust(iIndex, 1)
    ...
End Function

Function GetCustomerPhone(ByVal iIndex As Integer) As String
    ...
    GetCustomerPhone = sCust(iIndex, 6)
    ...
End Function

This leaves a modular abstraction that gives us all the benefits of the "modular is better" idea, while maximizing the "simple first, then elegant" concept.

Succinctness Is a Virtue

Another important aspect of this concept is not to overdo the naming conventions. The general rule is to use common sense when naming objects and abide by the rule of consistency. Here are some other rules: Don't require children to encapsulate their parents (for example, column names shouldn't have table names in them always). Stored procedures don't need every sorting and grouping in the title. Controls don't need the form name in their name. Functions don't need to spell out every detail of what they do. Remember, be consistent.

Finally, don't create tasks that try to do too many different things in the same function. Break your tasks into pieces. This will help outline the transactional logic. It usually adds steps, but the steps can be made succinct enough to be worthwhile. Consider the following:

Function UpdCustomer() As Integer
...If Not GenMatchcodePart1() Then Exit Function
    If Not GenMatchcodePart2() Then Exit Function
    If Not GenTransaction1() Then Exit Function
    If Not GenTransaction2() Then Exit Function
    If Not SaveCustomerAddress() Then Exit Function
    If Not SaveCustomerTransaction() Then Exit Function
    If Not SaveCustomerData() Then Exit Function...
End Function

This code has been nicely modularized and looks clean, but this one function has a lot of conditions in it. If you ever need to change how a matchcode is generated (say now it only has one part in it), that would necessitate changing a critical transaction section. Consider the following:

Function DoUpdCust () As Integer
...If Not GenMC() Then Exit Function
    If Not GenTran () Then Exit Function
    If Not UpdCust () Then Exit Function...
End Function

Function GenMC() As Integer
...If Not GenMC1() Then Exit Function
    If Not GenMC2() Then Exit Function...
End Function

Function GenTran() As Integer
...If Not GenTran1() Then Exit Function
    If Not GenTran2() Then Exit Function...
End Function

Function UpdCust() As Integer
...If Not UpdCustAddress() Then Exit Function
    If Not UpdCustTran() Then Exit Function
    If Not UpdCustData() Then Exit Function...
End Function

In this implementation, it is a little easier to follow what is happening when stepping through the code. Also, at a later time, you'll have only one part to a matchcode or transaction generation that only touches one function. The same is true of the customer save facility. If you need to do some different processing (such as adding a SaveCustomerDetail function), you only have a single section outside the critical transaction loop.

Also, notice the shortened and standardized naming convention for functions: The Do prefix for transactional loops, the Upd prefix for a type of task.

Recycle, Recycle, Recycle!

Recycling is a major part of developing quality code. It consists of writing code that can be reused and reusing code that has already been written. The implementations of this code recycling are varied. Some developers might use libraries of old projects, or modules of code, or even just function snippets. Others choose to just cut and paste from work they have previously done. Still others borrow only from respected sources or from CDs of published code. There are many benefits of reuse. Because these benefits are listed everywhere (including earlier in this article), I won't go into it further.

Reusability is enhanced by writing modular code and by using consistent cross-application naming conventions. The implementation of this extends far beyond Visual Basic code into database code, stored procedures, triggers, and so on. After all, it is one thing to reuse pieces and parts—it is yet another to be able to reuse entire projects.

Ask First, Then Code

This is really about a consistent way to remember to recycle. Before new custom code is written, you should investigate existing sources of similar code or functionality. Even if the current code was written in a custom, nongeneric way, reusing it might save some typing. Also, the time savings might present an opportunity to modify the existing base into a reusable code fragment.

When writing code, remember to keep other uses for functions in mind and generalize accordingly. This is generally something everyone could do more of. The main roadblock to doing this is the "not written here" syndrome. Developers tend to be apprehensive about using code they didn't write. However, if everyone is using these concepts, then all code should be easy to use and maintain. Only by implementing these types of standards can developers begin to break down the roadblocks on the way to reusable, maintainable code.

Validate, Validate, Validate!

When writing modular code, it doesn't hurt to validate at every step of the way, from the front end, through the data layer, and into the back end. If the data validation rules have a low rate of change (ROC), then it is wise to build validation routines all the way through the code—even to the front end. If possible, load the data rules from the back end or an .INI file.

When writing stored procedures, validate the data passed in before executing transactional logic. This will allow the procedure to better control error conditions. If all data has been pre-validated before an insert, no triggers will raise an error condition. This is not an argument for not using triggers. There should still be triggers—just don't count on them. By validating at every step in the process, you can maintain maximum control over the process flow at each step. This flow control lets you better pinpoint which pieces of data are causing problems at the earliest possible step in the process.

Using comprehensive validation techniques also opens new possibilities for constructing elegant error handling, logging, and reporting architectures. The flexibility and power of a stored procedure relative to a trigger allow for much more comprehensive error logging or returns.

Two Is Company, Three Is a Crowd

Write Boolean functions and procedures. This doesn't mean that all functions return true or false. But functions should always have a defined valid range and a defined invalid range (for example, pass or fail, true or false). This principle should extend to the database layer. Stored procedures and action queries should return values that fall inside or outside of a valid range. Consider the following code sample:

Function GetCustomer() As Integer
On Error Goto getcusterr
'Do some work

GetCustomer = iCustId
Exit Function

getcusterr:
    'handle error
    Error_Set
    GetCustomer = giErr 'some error number
    Exit Function

End Function

In this situation, you would expect to always get a valid customer ID or an error number. But without properly defining the interface, how would you be able to tell, inside the calling code, whether what was returned was an error number or a valid customer ID? And what does a zero value indicate? The trap here might not be obvious, but it can cause problems. Consider the following implementation:

Function GetCustomer() As Integer
GetCustomer = gicNoCust 'Some constant error number
On Error Goto getcusterr
'Do some work

If iCustId >= iCustIdMin AND iCustId <= iCustIdMax
    GetCustomer = iCustId
Else
    GetCustomer = gicInvCust 'Some constant error number
End If
Exit Function

getcusterr:
    'handle error
    Error_Set
    If giErr >= iCustIdMin AND giErr <= iCustIdMax
        GetCustomer = gicUknCust 'Some constant error number
    Else
        GetCustomer = giErr 'some error number
    End If
    Exit Function

End Function

This example is essentially the same. However, it is has been polished to include validation of the return data. This particular arrangement is a little bit of overkill, but it stresses the importance of validation and of distinction in returned values.