Three-Tier Architecture
This article appeared in the December 1996 edition of
EXE, and was a special report on the development of n-tier applications using a
Microsoft tool. The primary focus of this article specifically relates to
Visual Basic 4, the first version of the product to support this new
technology. The code listings are deliberately simplistic (rather than
production quality), although the downloadable sample code is a bit more
robust. The code is designed to run against RDO v1.

Client/Server development has traditionally been a
twotiered affair. That is, a client application directly interacts with a
data source and makes its own decisions as to how the data should be
manipulated. This approach has several drawbacks, the most significant being
that an entire data set often needs to be pulled across the network to the
client machine (which will be low spec in many cases), only to be discarded
once a result has been derived. Under these circumstances, result sets can get
paged out to disk and the application performance will suffer as a result.
Another unfortunate byproduct of this architectural approach is that the
code is very specific to the task for which it was written. If a similar
application is required by the same company which uses the same underlying data
sources then the same logic must be recreated, either from scratch or by
copying the code from the earlier application and then editing it as
appropriate. This is wasteful for the company, and also might result in a
duplication of any bugs which may exist in the original code. Worse still, any
general changes which need to be made to the code (e.g. to reflect a change in
the database structure) now needs doing (and testing) twice.
The three-tier model
Visual Basic 4 introduced the first implementation of a
threetier architectural model from Microsoft. This approach breaks down a
system design into three components, namely Data Services, Business Services
and User Services. The actual implementation of a three-tier system can vary
architecturally, but the logical tiers are described as follows:
Data Services
The Data Services component is the actual source of the data,
and may include such items as stored procedures and triggers. Typically, stored
procedures are the means to manipulate the data at the physical level, and
triggers can ensure that the referential integrity of the database is
preserved. In short, it is only concerned with the mechanics of data access.
Business Services
Business Services is a layer that acts as a wrapper for the
physical database. It is the component which actually causes the data
manipulation/processing. It also validates its actions against business rules.
For example, in a stock control system a user might be prevented from
reordering a specific item until a certain minimum stock level is reached. In
this case the Reorder method might be called but it would return an error
instead of initiating the action.
User Services
The User Services layer is typically the program run by the
end user this layer is also known as the presentation layer (because
thats all it should do). This component interfaces only to the Business
Services layer and all of its requests for data are satisfied from that level.
At no time does the User Services component directly communicate with the Data
Services component.
This approach to client/server design is described in the
product documentation as the Services Model. The philosophy behind this is that
the implementation of each layer is independent from the other 2 layers as long
as the interfaces that exist are clearly and rigidly defined up front. The
interface definition is called a contract. This tiered object architecture
provides a level of abstraction and truly promotes data hiding
both facets of the object oriented paradigm.
The means by which this is achieved is Automation. Previously
called OLE Automation, it is the term given by Microsoft to the mechanism
whereby separate object components can communicate with each other. This
communication can also take place between object components that exist on
different physical machines, in which case it is called Remote Automation.
These object components, using the definitions just given, are normally
referred to as Automation Objects.
Creating the server
Using the above definition of the three tier model, the use of
the term server can become a little ambiguous. To clarify this
point, I am referring here to the Business Services component.
It is first necessary to identify what the purpose of the
application will be, and to then specify these requirements as business
objects. These business objects can then be mapped to software objects, and a
logical hierarchy built for their access. Figure 1 shows the current design
best practice for an object hierarchy. It shows a top level interface called
Application, which is effectively a definition of the entire object set for
that server application. The Application object contains a collection of other
objects. In the example shown, an object exists (Customers) which is itself a
collection of single Customer objects.

Figure 1: Generic object hierarchy
One of the design goals for a Business Services component
should be reuse. Therefore when grouping together areas of functionality,
decide whether some areas are so unrelated from each other as to warrant
placing them in different servers altogether. This granularity is good practice
because it lends itself towards greater reuse within the business community.
With careful planning at a high level, a corporate environment can
theoretically make a rich set of business objects available for reuse by its
development community. Microsoft provide the Component Manager application with
the Enterprise edition of Visual Basic 4 to assist in this process. Tools such
as the Microsoft BackOffice Systems Management Server (SMS for short) can
ensure that these objects are always up to date throughout the enterprise
The object model that we are putting together therefore has
three classes. We can call the class at the top of the hierarchy
Application. Microsoft tend to use this convention a fair bit, but
its really a matter of personal choice. The Application class will be the
only object that can be accessed directly. All other classes must be accessed
via the Application class. In order to allow an external process to create an
instance of the Application class, it is necessary to set the Instancing
property (in the properties dialog for the class module) to either Creatable
SingleUse or Creatable MultiUse. See the separate panel for an explanation of
the differences between the two. The other two classes are more function
specific, and we will call them Customers and Customer.
To expose each interface from the Application object requires
that it be declared as Public, but with the Instancing properties set to Not
Creatable. Thus the Application class would expose an instance of the Customers
class in the declarations section as follows (note that I have deliberately
avoided adopting any naming conventions for readability):
Public Customers As New Customers
Strictly speaking, no
more code is required for the Application class. However, you
may like to expose other methods or properties such as the version
number of the server. Its also worth mentioning that the
use of the name Application for the top level class might not
always be suitable, depending on the scope of the server. If you
are defining a set of Business Services servers for an enterprise
implementation then a more unique set of names would be appropriate.
The Customers class is designed to mimic (where appropriate)
the behaviour of the predefined Collection class. This is a Visual Basic class
which is capable of storing other objects, and provides certain methods and
properties for accessing them. A simplified code listing for the Customers
class appears below.
Option Explicit
Private coll_Customers As New Collection
Public Sub Add(str_FirstName As String, str_Surname As String,
_
str_Address1 As String, str_Address2 As String, str_Address3 As
String, _
str_Address4 As String, str_PostCode As String, str_TelNo As
String)
Public Property Get Item(var_Key As Variant) As Customer
Set Item = coll_Customers.Item(var_Key)
End Property
Public Sub Remove(var_Key As Variant)
coll_Customers.Remove var_Key
End Sub
Public Property Get Count() As Integer
Count = coll_Customers.Count
End Property
The Customer class is little more than
a data container which either receives or returns the data by
using the new Let/Get Property statements. The code for this class
is shown below. Note that the actual data members are declared
as Private. This is where the strength of the class model can
be found: by using the Let/Get property statements, any modifications
to the data can be validated or refined before they are actually
applied. They can also be rejected if necessary by not updating
the internal data members, which illustrates the businessoriented
validation aspect. Thus the real data is hidden.
Option Explicit
Private mlng_ID As Long
Private mstr_FirstName As String
Private mstr_Surname As String
Private mstr_Address1 As String
Private mstr_Address2 As String
Private mstr_Address3 As String
Private mstr_Address4 As String
Private mstr_Postcode As String
Private mstr_TelNo As String
Public Property Get ID() As Long
ID = mlng_ID
End Property
Public Property Let ID(lng_NewValue As Long)
mlng_ID = lng_NewValue
End Property
Public Property Get Surname() As String
Surname = mstr_Surname
End Property
Public Property Let Surname(str_NewValue As String)
mstr_Surname = str_NewValue
End Property
Public Property Get FirstName() As String
FirstName = mstr_FirstName
End Property
Public Property Let FirstName(str_NewValue As String)
mstr_FirstName = str_NewValue
End Property
Public Property Get Address1() As String
Address1 = mstr_Address1
End Property
Public Property Let Address1(str_NewValue As String)
mstr_Address1 = str_NewValue
End Property
Public Property Get Postcode() As String
Postcode = mstr_Postcode
End Property
Public Property Let Postcode(str_NewValue As String)
mstr_Postcode = str_NewValue
End Property
Public Property Get TelNo() As String
TelNo = mstr_TelNo
End Property
Public Property Let TelNo(str_NewValue
As String)
mstr_TelNo = str_NewValue
End Property
Running the server
Taking the above as a real project, we now need to make a few
changes in order to get it to run as a server. When a new Visual Basic project
is created, a blank Form1 is always created with it. This can probably be
deleted because the server is unlikely to need to interact with a local user.
However, you will want to add a code module for global variable declarations
and global utility code. Note that it is more usual in an OLE server for the
application start point to be set as Sub Main, rather than having a startup
form. In our basic server model, no actual code is required, but the sub main
still needs to exist. In a reallife server though this would be the
logical point for specific initialisation code such as loading registry entries
(e.g. the location of the central error repository).
The application start point is set in the Project page of the
Tools, Options dialog, as shown in Figure 2.

Figure 2: Project page of Tools, Options dialog
This dialog also contains a few other settings of note:
-
Project Name: The string that is set in the Project Name field is used to
identify (in the registry) the classes that are defined as Public. This is the
Type Library name, and is the text string that is associated with the globally
unique id (GUID).
-
Application Description: This is the text that will be displayed in the
Tools:References dialog to identify the server (Figure 3).
-
Start Mode: The normal behaviour for
an OLE server is to terminate itself if there are no references
to it. Setting the start mode to OLE Server overrides this automatic
termination and keeps the server open. This is necessary because
the entries in the registry for applications that are running
from the development environment only exist while the server
is actually in run mode. If the server were to start up and
then immediately close down again because there were no references
to it then the client application would not find the servers
entries in the registry and would not be able to connect to
them.

Figure 3: Tools, References dialog
Once these are set, it is just a matter of selecting the Run
command and, correct syntax permitting, the server should be running.
Communication between the layers
Any software component built by Visual Basic which uses
automation will automatically include an additional software component, either
a proxy or a stub (or conceivably it could contain both). The proxy is
associated with the client, and the stub with the server (Figure 4). It is the
role of these agents to coordinate communications between automation
objects (the communication is performed via the Remote Procedure Call (RPC)
service), and to marshal parameter data to and from the process address spaces.

Figure 4: Proxy/stub communication on a single machine
If these processes reside on different machines then an
additional program (called the Automation Manager) needs to be running on the
same machine as the server component. This program handles the additional
processing necessary to ferry RPC calls across the network (Figure 5). Only one
instance of this program ever needs to run on the same machine because it acts
as a central clearing house for all such RPC transactions, and it will spawn a
new thread of execution for each RPC link that it is supporting.

Figure 5: Proxy/stub communication over the network
The automation mechanism as implemented
in Visual Basic 4 makes use of its own mechanism for handling
RPC calls across the network. Version 5 is bound to offer a DCOM
implementation as a result of this technology now being available
in Windows NT 4.
Error handling
Visual Basic, by nature, ideally needs an error handler to be
installed in each Sub or Function that contains code. This may appear to
increase the coding workload, but this process can be helped by using tools
such as AutoCoder which saves a lot of repetitive typing by adding code
templates to each Sub/Function. These templates can be configured to insert
company standard error handler boilerplates which, for each new piece of code,
can be modified to trap specific errors which could conceivably arise at
runtime. For example, a routine which opens files would always need to
trap error 53 File not found.
For the server application it is unlikely that you would ever
wish to display an error in a message box. In many cases server applications
will be running on a dedicated box in the computer room and so a popup error
message such as Type mismatch might not be seen until a user
complains that the application is not responding. Therefore, the error handler
should either deal with the error locally (if it can) or use the Err.Raise
syntax to pass the error up to the next item in the call chain, which is
probably the calling application. If it isnt, then each layer should fail
gracefully and pass the error back up the chain until the top layer is reached.
It is not possible to predict every error
that can occur, so allowance must be made for this by defining
some sort of catchall error code. Because a catchall
error trap can only return a general error value back to the calling
procedure, it is a good idea to implement some form of serverside
error manager which writes errors down to a central repository.
The client application can then report to the user that an unforeseen
error occurred in the server and that the helpline should investigate
it. This process can be of most use if you also allow the client
application to report a serial number to the user, which can relate
to the primary id of the error record. For database applications,
it is better to have this central error repository as an Access
database or even a flat file, because the error might have been
a problem reported by the Data Services component (e.g. a communication
link failure). Therefore, error handling should not be reliant
on other components.
Creating the client application
The client application is the representation of the User
Services component (presentation layer). Its role is to interact with the
Business Services component and display the results to the end user for viewing
or manipulation. Therefore it needs to obtain a reference to the Application
class and, through this reference (or other references derived from it),
interact with it.
The reference can be obtained via the
Set
As New <class>
statement. For example,
Dim myServer As CustomerServer.Application
Set myServer = New CustomerServer.Application
The object reference is duly used, (for example):
Debug.Print "Connected to CustomerServer.Application
version " & myServer.VersionNumber
After some usage, the reference is then deallocated:
Set myServer = Nothing
It is necessary, however, to tell the client application where
this CustomerServer object exists. To do this open the Tools:References dialog
(see Figure 3) and select the object entry which corresponds to the Application
Description that you gave to the server (Figure 2). In this case, the entry
.EXE magazine sample server is the correct reference so the check
box is clicked. For the server, it is worth using the priority button to raise
the entry to near the top of the list (Visual Basic forces two of its own
entries to the very top). This mechanism sets the order of the lookup of the
server name, and if you are using names that conflict with existing objects
from other sources then the server which appears highest in the list will get
the connection.
The customer server illustrates the key
point of the Client/Server issue, but is limited by its lack of
support for persistent data. It is possible to modify the existing
Customers class without changing any of the interfaces, and the
Customer class can be modified by the addition of one entirely
new module. Replacement code for the affected methods is shown
below. Note that the code in these listings is somewhat simplified
and includes no error handling. The sample code for this article
(see note at end of the article) is more comprehensive. I would
also like to make a point at this stage that the sample code uses
stored procedures on the database, whereas I could have used pure
RDO calls which have made for easier coding. The reason for this
approach is to show processing being performed at the Data Services
layer, which in turn emphasises the three-tier approach.
Public Sub Add(str_FirstName As String, str_Surname As String,
str_Address1 As String, str_Address2 As String, str_Address3 As String,
str_Address4 As String, str_PostCode As String, str_TelNo As String)
Dim obj_New As New Customer
Dim str_sql As String
str_sql = "execute sp_InsCustomer
'" & str_FirstName & "', '" & str_Surname
str_sql = str_sql & "', '" & str_Address1
& "', '" & str_Address2
str_sql = str_sql & "', '" & str_Address3
& "', '" & str_Address4
str_sql = str_sql & "', '" & str_PostCode
& "', '" & str_TelNo & "'"
Dim rdo_ps As rdoPreparedStatement
Set rdo_ps = grdo_Conn.CreatePreparedStatement("",
str_sql)
rdo_ps.Execute
Set rdo_ps = Nothing
SetupCollection
End Sub
Public Sub Remove(var_Key As Variant)
Dim rdo_ps As rdoPreparedStatement
Dim str_sql As String
Set rdo_ps =
grdo_Conn.CreatePreparedStatement("", "execute sp_DelCustomer
" & Trim(CStr(var_Key)))
rdo_ps.Execute
Set rdo_ps = Nothing
End Sub
Private Sub SetupCollection()
Dim lng_DelCount As Long
Dim rdo_ps As rdoPreparedStatement
Dim rdo_rs As rdoResultset
For lng_DelCount = 1 To coll_Customers.Count
coll_Customers.Remove 1
Next lng_DelCount
Set rdo_ps = grdo_Conn.CreatePreparedStatement("",
"{call sp_GetAllCustomers}")
Set rdo_rs = rdo_ps.OpenResultset(rdOpenKeyset)
While Not rdo_rs.EOF
Dim obj_Local As New Customer
With obj_Local
.ID = CLng(rdo_rs(SQL_CUSTOMER_ID))
.FirstName = rdo_rs(SQL_CUSTOMER_FIRSTNAME)
.Surname = rdo_rs(SQL_CUSTOMER_SURNAME)
.Address1 = rdo_rs(SQL_CUSTOMER_ADDRESS1)
.Address2 = rdo_rs(SQL_CUSTOMER_ADDRESS2)
.Address3 = rdo_rs(SQL_CUSTOMER_ADDRESS3)
.Address4 = rdo_rs(SQL_CUSTOMER_ADDRESS4)
.Postcode = rdo_rs(SQL_CUSTOMER_POSTCODE)
.TelNo = rdo_rs(SQL_CUSTOMER_TELNO)
End With
coll_Customers.Add Item:=obj_Local, Key:=CStr(rdo_rs(SQL_CUSTOMER_ID))
Set obj_Local = Nothing
rdo_rs.MoveNext
Wend
Set rdo_rs = Nothing
Set rdo_ps = Nothing
End Sub
Public Sub Update()
Dim rdo_ps As rdoPreparedStatement
Dim str_sql As String
str_sql = "execute sp_UpdCustomer
" & CStr(mlng_ID) & ", '" & mstr_FirstName
str_sql = str_sql & "', '" & mstr_Surname
& "', '" & mstr_Address1 & "', '"
str_sql = str_sql & mstr_Address2 & "',
'" & mstr_Address3 & "', '"
str_sql = str_sql & mstr_Address4 & "',
'" & mstr_Postcode
str_sql = str_sql & "', '" & mstr_TelNo
& "'"
Set rdo_ps = grdo_Conn.CreatePreparedStatement("",
str_sql)
rdo_ps.Execute
Set rdo_ps = Nothing
End Sub
Navigating the dots
While the Remote Automation architecture allows a tidy way of
developing threetier applications, performance can be a problem if there
is repetitive accessing of interfaces (i.e. the Object.Property syntax). For
example, if the customers example contained 50 records then it would be a
reasonable assumption that the new For Each...Next language construct would be
the correct course of action. This could be coded as:
Dim myCust As Customer
For Each myCust in myCustomers
myListBox.AddItem myCust.FirstName & " "
& myCust.Surname
Next
In fact, if this code was run as a client talking to a remote
server then it could take several seconds to complete the execution. Now
imagine a form which had several such list boxes. The user would have time to
go and get a coffee, and your reputation would suffer (although you would never
ship it in this state of course). So why is this method so slow? The reason is
that there is a constant overhead for the preparation of a single RPC
transaction between the proxy and the stub. Single events go unnoticed but when
this is magnified by a factor of 50 (in this case) then the effect becomes more
pronounced. Visual Basic 5 rumour pages on the Internet suggest that Microsoft
have worked hard in this area and have decreased the overhead quite
substantially. However a workaround needs to be used for the time being.
One way to overcome this problem is to build generic array
passing mechanisms into the server. If an array is created and populated by the
client application, and then passed by reference to the server, it can be
populated with data. The array is then read by the client application, and only
a single call was made to the server, rather than the 50 needed earlier. A
simple example of this array handling technique is shown below. It could of
course be extended to accept a variable number of parameters (using the
ParamArray syntax) to define specific fields to return. It would also be
possible to include range parameters to restrict the amount of data being sent
back into manageable subsets.
>>Client process (relevant lines):
Dim myArray() As String
ReDim myArray(myCustomers.Count, 8)
myCustomers.ItemList myArray
>>Server process:
Public Sub ItemList(ByRef arr_Data() As String)
Dim int_CurrentRow As Integer
Dim myCust As Customer
int_CurrentRow = 0
Other performance issues
There is a certain amount of tuning which can performed in
order to optimise the threetier configuration. The network infrastructure
obviously plays a key role in performance. The choice of data access model
depends upon the scope of the system. If everything is intended to reside on a
single machine and the database resides locally (e.g. an Access database), then
DAO would be the ideal choice. RDO would be the more normal choice for server
based data services (e.g. SQL Server) because it is designed to support the
object model, although for the brave of heart the ODBC API is slightly faster
and gives a greater degree of control over what you are doing. Beware though
that you can get into difficulty more easily (and more substantially). The
other Microsoft data access mechanism, the VBSQL.OCX control, has apparently
had its day and is no longer being developed.
The amount of processing required of the server component and
the actual performance of the CPU should be weighed against the throughput
capability of the network infrastructure, because it may be beneficial to
either have the business objects on an entirely separate machine from the
database server, or to have them reside on the same machine.
The server component can be declared as Creatable SingleUse or
Creatable MultiUse.
Finally, it is worth considering a pool manager application to
reduce the startup time of objects. For example, if an object is declared as
Creatable SingleUse (one process per client application) and it needs to
initialise itself with data from the data services layer, then a new connection
by a client could have a noticeable delay while the new object starts itself
up. A pool manager is designed to maintain several extra running objects, and
thereby overcome the startup delay. Visual Basic provides a sample pool manager
as a springboard.
Raw performance increases are likely to
be one of the features of the forthcoming Visual Basic 5. The
fact that Visual Basic now has more substantial competition in
the form of Delphi should have given the software giant the motivation
they needed to speed things up. I hope.