Client/Server Solutions: The Design Process

Ken Bergmann
Microsoft Developer Network Technology Group

September 29, 1995

Abstract

This article illustrates some fundamental concepts that should be considered when designing 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.

Introduction

In the first article in this series, I discussed the Layered Paradigm, a standard paradigm I recommend for client/server applications built using Visual Basic. The Layered Paradigm is a method of application architecture that divides the component operations of an application into several component pieces.

Building an application using the Layered Paradigm can be complicated and challenging. Sometimes it is appropriate to design your application using vertical layering, and sometimes it is appropriate to use horizontal layering. In this article, I'll try to clarify how and when to choose each type of layering by illustrating a "best case" implementation approach.

The "best case" approach is not meant to be seen as the complete answer to all client/server problems. A "best case" solution is simply a solution that attempts to solve the major issues in a client/server scenario while continuing to provide leverage for creating more complex solutions. These solutions don't answer custom problems, and they don't implement a "pure object theory." They do, however, provide simple, elegant solutions to the most common problems faced by the client/server application developer. And they address these issues in a way that capitalizes on the benefits of the Layered Paradigm and good coding practices.

"Best Case" Approaches: A Technical Overview

"Best case" solutions are solutions that address common real world problems and provide a customizable code base for creating custom solutions to real-world problems that are application-specific. In other words, "best case" solutions break all specific problems down into the parts they have in common with other problems. They then form solutions for the common parts only. By uniting the solutions for the common parts, they can more easily provide solutions to all parts of the specific problem and still provide a framework by which all specific problems that are similar may be solved.

"Best case" solutions handle the hardest problems as easily as a custom solution would while handling the common problems more easily than most approaches. "Best case" solutions also handle the easiest problems with tremendous improvements in all areas of the development and production cycle.

In summary, an application solution that uses "best case" methods will always be as good as a custom-coded approach given the same problem domains. And a solution based on common solutions will be an order of magnitude better for each facet of the common solution that is incorporated into the application-specific solution.

The goal of a "best case" solution is to solve the basic problem in general terms and then customize the common solution to solve the specific problem domain. The diagram in the next section illustrates some of the goals of software development and the areas that a "best case" solution addresses. It also shows which areas are addressed by custom implementation approaches.

"Best Case" Definitions

Figure 1. A graphical depiction of the effects of using a "Best Case" solution

As is shown by the illustration, any solution that will meet all five goals will cost more to develop for true reuse than the worth of the actual code developed (first-time usage only!). As any software engineer will agree, reusable code is always front-loaded, that is, it is substantially more costly to develop code for reuse than to develop code to solve a specific problem. However, notice that by using a "best case" solution, the design time was used in the best way and allowed the code to be more easily maintained and portable.

What Are the Implications of Implementing "Best Case" Solutions?

Essentially, the way to leverage your design-time forward through to portability and maintainability is through recycling. Recycling is designing and writing code to solve the common problems of client/server development without solving the specific problems of a particular application. This by no means excludes designing or writing code that solves specific problems. It just means that solutions are first created (or borrowed) to solve the most basic problems and then are slightly modified to meet the problem domain of a specific application.

Where Does "Best Case" Meet Reality?

A classic example is the loading of data from a database access call into a graphical list box control. This is done over and over in different problem domains, each time in a slightly different way. Some projects consolidate this into one call per form; others take the vertical approach and write a routine to load the control as a function that is part of some object grouping.

The first solution, using the Data Access Objects (DAO) as a transport for simplicity, writes a function like this:

Sub FillCustomerData()
...database stuff
Do Until ssTemp.EOF
    cmbStuff.AddItem ssTemp("StuffDescription")
    cmbStuff.ItemData(cmbStuff.NewIndex) = ssTemp("StuffId")
    ssTemp.MoveNext
Loop...more db stuff
Do Until ssTemp.EOF
    cmbJunk.AddItem ssTemp("JunkDescription")
    cmbJunk.ItemData(cmbJunk.NewIndex) = ssTemp("JunkId")
    ssTemp.MoveNext
Loop...
End Sub

This is encapsulation, right? The database access and the front end have been separated! Wrong. This is just organization. Encapsulation must provide some useful interface to distinct processes or entities within an application. This doesn't really do either, but it is a step in the right direction.

Our second solution writes functions like this:

Sub FillCustomerNameText()
Sub FillCustomerPhoneText()
Sub FillCustomerStuffCombo(ByVal iCustID As Integer)
...database stuff
Do Until ssTemp.EOF
    cmbStuff.AddItem GetCustomerName(iCustID)
    cmbStuff.ItemData(cmbStuff.NewIndex) = iCustID
    ssTemp.MoveNext
Loop
End Sub
Sub FillCustomerJunkCombo(ByVal iCustID As Integer)
...database stuff
Do Until ssTemp.EOF
    cmbJunk.AddItem GetCustomerJunk(iCustID)
    cmbJunk.ItemData(cmbJunk.NewIndex) = iCustID
    ssTemp.MoveNext
Loop
End Sub

Now surely this is encapsulation, right? The database access has been separated from the front end, and I now have a set of functions that encapsulates the customer object functions! Wrong. Once again in this example, we are just reorganizing code. We won't be able to consistently recycle the design or the code that went into this solution. We must still write the code for filling the combo boxes, and we must still completely walk through the design for an object with customer-like properties. We cannot reuse the implementation of the interfaces because the properties are slightly different. This solution also refuses to be recycled.

Isn't Pure Reuse the Best??

According to some reasoning, the encapsulation of the second solution above is more beneficial than any recycling. The argument some developers use is that because all the operations that operate on a customer have been encapsulated, pure reuse of this object will be possible. That argument has some merit. By tightly encapsulating a custom implementation, pure reuse does become possible. But with what benefit? Only an application that has exactly the same customer usage requirements can utilize this type of encapsulation. The many other applications that might benefit from leveraging some of the implementation of this design are simply unable to! This is because there are no recyclable qualities in the design. This type of implementation only makes it harder for any reuse (especially the impure type!) to occur from application to application or even within an application.

Recycling doesn't strive to support pure reuse, but rather to provide a code base that can be leveraged for solutions to similar but different problem domains. Consider the following solution:

Sub FillCustomerData()
...some code to get customer ids and keys
txtCustName = ExecSQLGetText(sGetCustomerName & sKey)
ExecSQLFillLBItem cmbStuff, sGetCustomerStuff & sStuffKey
ExecSQLFillLBItem cmbJunk, sGetCustomerStuff & sJunkKey
End Sub

This would be a nonoptimized solution without object encapsulation. Notice that there would have been defined a standard for database access that exposed the functions called here. Since this solution obviously does not show all object operations being encapsulated, here is the revised solution using object encapsulation functions:

Sub FillCustomerData(ByVal iCustId As Integer)
CustomerNameGet txtCustName, iCustId
CustomerStuffGet cmbStuff, iCustId
CustomerJunkGet cmbJunk, iCustId
End Sub

Sub CustomerNameGet(ctlMe as Control, ByVal iCustId As Integer)
Dim sQry as String
sQry = GetNameQry(iCustId)
ExecSQLFillCtl ctlMe, sQry & Str$(iCustId)
End Sub

Sub CustomerStuffGet( ctlMe as Control, ByVal iCustId as Integer)
Dim sKey as String
Dim sQry as String
sKey = GetStuffKey(iCustId)
sQry = GetStuffQry(iCustId)
ExecSQLFillLBItem ctlMe, sQry & sKey
End Sub

In this example, the standard database calls would be encapsulated in customer object operation functions. Using this technique, the only knowledge about customer object structure would reside in these customer object functions.

These examples do not specifically make use of known "best case" solutions; however, they do emphasize that by using a recyclable implementation, it is possible to leverage any code base and still maintain the benefits of encapsulation and object orientation. Recycling is the essence of a "best case" solution. All "best case" solutions are recyclable by definition.

The Full Impact of Recycling

Recycling doesn't just impact how functions are organized and where data is stored. It impacts how data is stored, the naming conventions used for all parts of the system (database objects, stored procedures, modules, forms, and controls), the functionality of stored procedures and queries that are executed, and the organization of the business and transactional logic of the application.

A Recycling Example

Consider a simple database front end. This front end accesses a SQL Server by executing stored procedures using DAO as a transport. The focus of the front end is to modify the records for several "objects" in the database. For the sake of simplicity, we will define the objects as follows:

Simply put, the front end must have the following features (in no particular order, but numbered so that the discussion that follows can refer back to them):

  1. A multiple-document interface (MDI).

  2. The ability to perform login procedures to any designated SQL Server.

  3. The ability to check front end version with database version upon database connect.

  4. The ability to log all errors to a file.

  5. The ability to log all SQL statements to a file.

  6. A child window to view, insert, edit, and delete resources.

  7. A child window to view, insert, edit, and delete skills.

  8. A child window to view, insert, edit, and delete managers.

  9. A child window to view, insert, edit, and delete tasks.

This situation is about as common as can be found. I have deliberately defined it in simple terms in order to emphasize the thought process involved in breaking the components down. In the next section, I will discuss how to engineer a "best case" solution for this application.

The Recycling Solution: Step One

The first step in engineering a "best case" solution is to identify any common problems in the specific application problem domain. In this example there are several. In fact, every single requirement is in some way a common problem! Requirements 1 through 5 are common to client/server applications. Requirements 6 through 9 are common to each other. The functionality of requirements 6 through 9 is also common to client/server applications. Being able to see these commonalties is the first step towards engineering a recyclable solution.

The Recycling Solution: Leap Two

The second phase is to design the components that will provide solutions to the common problems. Based on the previous requirements list, I will condense these into the common problems identified as follows (in no particular order, but numbered so that the discussion that follows can refer back to them):

  1. Manage an MDI interface, that is, status bars, toolbars, menus, and so forth.

  2. Provide a generic login window to collect login information for server systems as well as files.

  3. Store and retrieve version information from a generic database object (table).

  4. Manage a log file for errors.

  5. Manage a log file for SQL statements.

  6. Manage child windows.

  7. Retrieve data from database.

  8. Manage database transactions.

In the following paragraphs, I will outline some methods for addressing these issues. It's important to note that the key component in this phase of the recycling process is to take the current set of requirements and condense them. (Keep in mind that these concepts do not comprise the only solution—just one of many solutions.)

1.  Provide a generic MDI parent

To do this may require addressing the following issues:

Providing a generic MDI parent is done by specifying variables and functions or subroutines by which the MDI parent can configure itself at run time. For example, if the behavior of a toolbar is to be configurable, a routine to set the properties of the toolbar should be defined. If there are standard variable properties that may be used (like constants for the status display or a flag for an application wide setting), these should be defined. Instead of just hard-coding values in place or writing a custom toolbar configuration routine, think about how it can be generalized—that is, identify the tasks that will have to be done from application to application. Then, consolidate these types of operations.

2.  Provide a generic login window

To do this may require addressing the following issues:

Providing a generic login window is done by specifying what the standard login window will look like and how it can be configured. For example, what controls are displayed for a server type of login as opposed to a file type login. Are there graphics or information (like version numbers) that need to be displayed? These interfaces should all be defined in a generic sense. Don't hard-code versions or captions! Provide a function or variables to set the properties of the window. Expose an interface to decide at run time which controls to display. If it can be generalized once, it will not need to be done again.

3.  Define a version control system

To do this may require addressing the following issues:

It is important to ensure that a solution involving an external system (SQL Server or Microsoft Access file) be abstracted enough to allow a system substitution with little impact on the application. If it is conceivable that the application may need to utilize both a Microsoft Access file and a SQL Server database, then storing any version information in a stored procedure would be portable from the SQL Server implementation only in the form of a predefined Microsoft Access query. This may not have the features available in a stored procedure; therefore, this type of generalization is not recommended. Recycling solutions should always be designed in as abstract a way as possible. Recycling solutions always keep in mind component substitutions. Being able to keep these substitutions in mind is why properly defining interfaces is so essential to creating a recycling solution.

4 & 5.  Manage log files

To do this may require addressing the following issues:

As should be apparent, the commonality between problems 4 and 5 was identified. Because the task is the same and only the data content is different, one and only one solution is required. This is a very good expression of recycling at work. By defining one solution with enough abstraction, multiple problems can be solved simply and elegantly. The added benefits are that the solution is consistent through all uses. The interface to an error log is the same as the interface to the SQL statement log. We have a simple solution for two problems, and we get maintainability and portability for free! This type of solution consolidation will be discussed again later.

6.  Manage child windows

To do this may require addressing the following issue:

Managing child windows is simply thinking through a standard, recyclable way in which a configurable number of child windows may be managed and instantiated. This section might actually be designed as a function of the MDI parent window. There are many schemas for this—the idea is to choose the one that solves the common problems and then to use that solution to leverage a custom solution.

7.  Retrieve data from database

To do this may require addressing the following issues:

Retrieving data from a database is the second biggest problem that is addressed in client/server development. Because it is such a large section of the development work, it deserves the most consideration. For now, the following summary will suffice to present the general concept.

In a well designed client/server system, there are several main categories of data access models that are common in client/server applications. They are as follows:

This particular application is a simple online data manipulation tool, and the discussion will focus on that specific category. Within that category, there are several basic types of data retrieval that are commonly done in a client/server system. They are identified for this discussion as follows:

With the common data retrieval methods identified, the next step towards implementation is to classify the variations that will be required for the solution to become recyclable. To accomplish this, the common tasks that these types of data retrieval support must be identified. By using the term common, it is implied that these are tasks that may be repeated and may be implemented in a configurable way. Customized implementation is not the goal of this step. The following is a simple breakdown of the required tasks:

The previous step is included simply to show the progression from concept to design and then to implementation. It was not meant to be the complete working through of a data retrieval architecture. For this level of detail, see the section "What Is the External Access Interface?" in the previous article in this series, "Client/Server Solutions: The Architecture Process."

8.  Manage database transactions

To do this may require addressing the following issues:

This problem is the single biggest problem addressed in client/server development. Because it involves such a large portion of the development effort, we won't in this discussion attempt to address all the possible ramifications of this problem. This discussion and the following section will, however, illustrate how to engineer a solution for this particular application.

Since the external database component (SQL Server) of this particular application supports stored procedures, the interface to database transactions can be drastically simplified. The following is a list of each major type of transaction and its interface specification:

So the actual designed interface at the database access level would simply be one routine: Execute the procedure and return the code. If the return is zero, indicate success; if the return is non-zero, handle as an error condition. (This is the hook for adding localization or error lookups.)

Seeing the impact of decisions like this one allows for effective recycling on a project-to-project basis. The crucial leap is to see that the first and most important step is always simplification down to a base form. Customization at any time afterwards becomes merely an academic exercise. The standards for database access, for form-level organization, for service management, and all other components, are set forth by simplifying common problems. Once the standards are in place, it is possible to capitalize on them in a recyclable way—though not necessarily in a reusable way. Of course, solutions that are simplified enough and common enough can be made reusable. The path to making these solutions reusable becomes that much shorter because the exercise of generalization (which is the most complex part of the process) has already been accomplished. But unlike reuse, the solution is not committed to 100 percent reuse. At no time does the situation become an all-or-nothing decision. The solution can be recycled as much or as little as desirable with absolutely no impact on other users or implementations of the solution.

The Recycling Example Concluded

This example is not the only possible solution to the proposed requirements. It simply illustrates the processes, concepts, and priorities that, taken as a whole, comprise the recycling approach. Following are some guidelines to consider.