Client/Server Solutions: The Architecture Process

Ken Bergmann
Microsoft Developer Network Technology Group

September 29, 1995

Abstract

This technical article illustrates some fundamental concepts that should be considered when architecting client/server solutions in Visual Basic®. It is geared toward developers who are providing client/server business solutions using the Visual Basic development environment. The concepts in this article are particularly useful for developers with an understanding of transaction processing systems.

The Layered Paradigm: A Technical Overview

The standard paradigm recommended in Visual Basic® client/server applications is that of a series of layers. In this section, I will explain why I recommend the Layered Paradigm as a standard, what its strengths are, and where it breaks down.

The Layered Paradigm divides the component operations of an application into several component pieces. The component operation becomes the component level worker, as opposed to a core function. Before I talk about why this is best, here are some definitions for the terms I am using:

Developing an application built of components—object-oriented programming (OOP), Component Object Model (COM), and so forth—is a popular thing to do, but within the constraints of the Visual Basic programming arena, it can be exceedingly difficult. The performance and memory limitations of Visual Basic often stand in the way of developing successful applications based on component pieces. The Layered Paradigm works around this by providing a threshold that allows you to divide components into groups by their relative "atomicity." The Visual Basic programming arena requires a higher threshold when dividing component operations into a component's core functions, as opposed to the C or C++ programming arenas, where the construction of a component's core functions can be very atomic. By moving the threshold from an atomic core function to an atomic component operation, the application receives the benefits of component design while staying within the constraints of the Visual Basic programming arena.

Real-World Application

The smallest unit in Visual Basic that meets the above definition is the module. A module consists of code that is loaded and unloaded at the same time, can include the same scoping, and is modular in its interface. An OLE object instantiated from within Visual Basic code has similar characteristics. All the code in the object is loaded and unloaded with the instantiation of the object. It has its own scoping and has a modular interface.

Interfaces like these items, which are the lowest atomic units in the Visual Basic programming arena, can (and, under a true component model, would) be broken down into their functional pieces. For example, the ExecSQLBool function is unwrapped, so that the SaveCustomer method becomes a series of database transport function calls that together constitute the functionality of executing structured query language [SQL] statements to retrieve a Boolean result.

However, doing this isn't always an optimization. Often the application architecture will degrade in other areas (for example, every piece of the application that used ExecSQLBool now has to do its own database transport function calls). And what happens when the architecture changes—for example, if the database transport functions are now instantiated as methods on an OLE object? There would then be significant overhead involved with repeated instantiations of the SQL OLE object, compared to creating one instantiation and then passing everything through to that one instantiation.

When modifications are made to implement this new approach, it takes much longer because the database transport code is spread out through the application-specific code. When the modification is finally finished, there is only one instantiation. That object is passed around to all the components of the application, so they can use it directly. There is still substantial overhead involved with dereferencing the object with each pass to a different piece of the application, so performance is still not acceptable, and memory usage soars.

The Layered Paradigm, however, evens this scenario out nicely. Because there is only one instantiation, no other component has to spend time in dereferencing pointers, crossing application space, or other costly maneuvers. All the code for a component operation (for example, the ExecSQLBool function, which is simply the making of a series of database transport function calls) is in one place, so any modifications are easy and localized. Memory is optimized because all the code for this component operation is unloaded when not being used. The Layered Paradigm provides all the benefits of a component design while side-stepping the unwanted frustrations that come from Visual Basic's limitations.

Pros and Cons

The Layered Paradigm has many benefits that help meet useful objectives in the changing world of client/server application development. Recognizable, measurable benefits include the following.

As with any paradigm, there are also limitations. Some potential dangers are:

The Layers of a Visual Basic Application

The standard layers of a Visual Basic application are:

A graphical representation of these layers follows (Figure 1).

Figure 1. An example of a layered architecture

The following coding example outlines how an update to a customer record is processed from the front end all the way through the database. This example is intended to show how the control of a process can be subdivided into the four (4) layers of the application.

User Interface

The user interface is the only portion of the application that is responsive to user interaction. The user interface is where all data is presented to the user by means of window objects. The user interface is also where all inputs or modifications to data are made by means of window objects. The user interface should be one of two layers that references window objects (controls, forms, and so forth). In the best case scenario, the user interface would be the only layer that references window objects. However, in reality, allowing the data interface to reference the window objects directly can significantly reduce the amount of code and the simplicity of code paths. Since there are many aspects of a design that must all balance out (maintainability, reusability, performance, and simplicity), tradeoffs such as this are often considered.

What should the user interface include?

The user interface includes all event handlers or events. These subroutines are called in response to some user action (a click, a mouse move), to a change in status of a window object (Form_Resize, Lost_Focus), or to callback procedures (VBSQL_Error, Timer). It also includes procedures that either fill controls with data or retrieve data from controls. As stated above, the line between the user interface and the data interface is often very fine when discussing the issues of filling controls or retrieving data from controls. The responsibilities of the user interface include operations such as:

What should the user interface not include?

The user interface should not be responsible for operations such as:

Data Interface

The data interface is where an application completes all its in-memory data manipulation. The data interface is responsible for validating and manipulating all an application's data. The data interface supplies all data to the user interface for display in window objects. The data interface supplies all data to the transaction interface for use when supervising the external access interface. This would include SQL strings and parameters. The data interface may use locally stored data sources such as registry entries or caches to store operational parameters and data. However, all external access should be done through the transaction interface.

What should the data interface include?

The data interface includes any routines that will perform operations on the data of an application. The responsibilities of the data interface should include such operations as:

What should the data interface not include?

The data interface should not be responsible for such operations as:

Transaction Interface

The transaction interface is part of the working internals of a client/server application. The transaction interface controls all data accessed by the application from an external data source. In addition, the transaction interface controls all updates to the data in the external data source that are initiated by the application. The transaction interface uses the external access interface to process its communication with the external data source and uses the data interface as its application data repository.

What should the transaction interface include?

The transaction interface oversees the external access interface in the transfer or manipulation of all data to and from an external data source. The responsibilities of the transaction interface should include such operations as:

What should the transaction interface not include?

The transaction interface should not be responsible for such operations as:

External Access Interface

The external access interface embodies the communication of an application with an external data source. The external access interface is the specific transport or transports that the application uses to communicate with an external data source. Some common transports are DB-Library, Remote Data Objects (RDO), and Open Database Connectivity (ODBC). These transports require different code to complete specific functions. By encapsulating a series of external function calls or methods within modular functions, you can build reusable code that reliably completes certain component operations. For example, the component operation of logging into a database can be encapsulated in some general fashion. This same code can be used without modification in every application that uses an identical transport. The architecture used to call this function can be used even in applications that use a different transport. This preserves the consistency of a code base and allows for enhanced reusability and extensible architectures.

There are several benefits to coding a specific external access interface, the first of which is the demonstrated benefit of reusability. A second benefit derives from an application having a central pipe where all its external communication is completed. The ability to tap into a central location for logging and error handling can be a tremendous time saver.

A third major benefit is that encapsulating transport-specific code makes it extremely easy to replace an existing transport's code with code for a different transport. This method also allows an application to make modifications to the implementation of a transport without modifications to the application-specific code.

What should the external access interface include?

The external access interface directly handles all communication with an external data source. The responsibilities of the external access interface should include such operations as:

What should the external access interface not include?

The external access interface should not be responsible for such operations as:

External Component Interfaces

External component interfaces are components that exist outside an application boundary and provide some service to those applications that instantiate them. External component interfaces are becoming much more common with the onset of OCX controls and OLE Server functionality in Visual Basic.

Where do external component interfaces fit into the Layered Paradigm?

With so much functionality being encapsulated and reused in these external component interfaces, the need to position these within the Layered Paradigm is very real. Fortunately, the Layered Paradigm facilitates the use of such components with remarkable ease. External component interfaces are positioned entirely based upon how much ownership they exert over their own display mechanisms, data storage and manipulation, transactional logic, and external data access. If an external component interface is responsible for its own data (for example, an object encapsulating customers), it might require an application's data interface to interface with its data. If it only manipulates data (for example, calculates tax for a purchase in a certain state), it might not require the data interface to interface with it at all. It might, however, require the transaction interface to interface with it (for example, generate a match code) or the external access interface to interface with it (for example, SQL OLE connection).

As should be obvious at this point, external component interfaces fit in smoothly with the Layered Paradigm. The only point that deserves special consideration is that of consistency. When you use an external component interface, I would recommend that your application implement a standard, reusable set of core functions if at all possible. This consistency throughout the application will add significant value when code bases are compared, code reused, and complex applications debugged.

A Note to C++ Developers

As a C++ developer, you might find yourself initially at a loss when you begin to think about object-oriented design in Visual Basic. Since Visual Basic is not a true object-oriented language, how can you use it to implement a true object-oriented design? Furthermore, as Visual Basic becomes more object-oriented in future versions, how do you make sure that your implementation today doesn't preclude a safe and easy transition to the future versions of Visual Basic? The answer to these questions lies in the Layered Paradigm that I've presented here.

Of all the aspects of an object-oriented language, only data encapsulation has relevance in Visual Basic. (For example, Visual Basic classes have no ability to inherit functionality from one class to another. However, you can still provide an object-like interface by doing strong data encapsulation. Visual Basic has the ability to scope variables at a module level and allow private functions in a module. This enables you to define private data and methods that can help to encapsulate data from the rest of the application.

This approach fits well in a layered architecture, since the data interface of an application can be divided into separate components, each of which deals with a particular type of real-world data. In C++, objects encapsulate their data completely and often do not service user-interface commands—like filling a list box with a set of results. However, in Visual Basic, every time a function is called in a module, that module is loaded into memory, and when the function is finished, the module is unloaded. The overhead in calling a function is much greater than in C++, so tradeoffs must be made for performance reasons. The data interface can still strongly encapsulate its data, but it also should be able to accept list boxes and other user-interface controls to populate or to be able to pass entire structures to and from the user interface.

The Layered Paradigm uses three terms that may seem confusing to a C++ developer: component, component operation, and core function. These terms are intentionally different from common object-oriented terms because Visual Basic isn't as flexible as an object-oriented language, and design must be done in a slightly different way. However, they are fairly analogous to object-oriented concepts, and for the purposes of discussion, we will think of them in C++ terms as object, object method, and nonspecific method.

A component can be compared to a C++ object in the sense that it contains data (some private and some public) and methods to manipulate that data. However, it differs in that a component may be a single .BAS file and, in addition, has no ability to be instantiated as multiple objects, each of which contains its own copy of the object's data. Instead, a component contains just one copy of data or can contain an array of data structures, but it is entirely the responsibility of the component to maintain that data.

Component operations can be compared to C++ methods, but it is important to note that in Visual Basic they do not carry an implicit "this" pointer. Since that is the case, there is no reference to a specific instance of data. It, therefore, falls within the responsibility of the client code to let the component operation know which data to operate on. If a component is designed to support only one instantiation of its data, this construct is fairly simple, but if this component needs to have many instantiations of its data, then instantiations of data structures can serve to keep the data separate.

A core function can be compared to an unattached method in C++. This is a function that has no object reference and is independent of any particular data. It merely does a job and does not encapsulate any data. A C++ example of this would be a Windows API function. Though you could argue that all methods should belong to an object, there are times in Visual Basic where this approach is both unnecessary and improper.

In the end, at whatever level the Visual Basic application is designed, the architecture should always strive to encapsulate data and abstract complexity from the user interface. Using this approach will also decouple the user interface from the actual data format and will provide a scalable and maintainable design.