Thin-client hardware and architectures have been discussed on this site. Related topics include Reasons To Use Two-Tier Fat Client Instead of Browser-Based Application Development Technology, JDBC Brings the Fat Client to Java, and IE4: The First Fat Client Browser. Still, I frequently get questions along the lines of "How do I write a thin client application?" or "How do I know if my application is a thin client?". This brief provides some thin client coding tips.
Do not be fooled into thinking that these tips have no bearing on your work if you are not writing thin client software today. Even if you are deploying two-tier, or even single-tier, applications, many of these tips will help you produce better software.
Thin clients are often discussed in the context of three-tier architectures. In the typical simple description of three-tier, the tiers are labeled Presentation, Business and Data Access, or some trivial variation thereof. So, what does it mean to write the Presentation layer? Well, the code should be responsible two things:
The presentation layer can also be bounded by what it does not do:
Even if you don't write thin clients, applying thin client discipline will allow you to produce more adaptable software.
I especially believe in disabling and enabling controls based on whether they are valid. For example, a Save command button should not be enabled if a required field is empty. Therefore, I use code like the following.
Private Sub Form_Load()
SetCommandStatus
End Sub
Private Sub txtRequired_Change()
SetCommandStatus
End Sub
Private Sub SetCommandStatus()
' This would usually be more complex logic
cmdSave.Enabled txtRequired.Text <> ""
End Sub
I believe in limiting the number of calls strictly to one because any sequence of calls is probably a business rule or business process in disguise. For example, the following code is executed when a new item is added to an invoice form.
Private Sub DBGrid1_UnboundAddData(ByVal RowBuf As MSDBGrid.RowBuffer, _
NewRowBookmark As Variant)
Dim curItemPrice As Currency
Dim lngItemMult As Long
Dim objItem As New Item
On Error GoTo UAD_Error
lngItemMult = RowBuf.Value(0, 0)
objItem.Key = RowBuf.Value(0, 1)
curItemPrice = objItem.GetPrice(m_oCustomer, lngItemMult)
m_oInvoice.AddItem objItem, lngItemMult, curItemPrice
NewRowBookmark = objItem.Key
Exit Sub
UAD_Error:
MsgBox Err.Source & vbCrLf & Err.Number & ": " & Err.Description
RowBuffer.RowCount = 0
Err.Clear
End Sub
This is a pretty simple routine, but it encapsulates both a business rule (the
price may be customer- and quantity-specific) and some implementation details
(an item knows its price, the row's bookmark is the item key). The rule and
implementation details must exist somewhere in the code, but an event handler
is not the right place, for at least two reasons. First, the rule and details
are likely to change, and one reason to use a thin-client architecture is to
minimize the impact of changes on the configuration management of the client.
Second, the rule and details may well be needed on more than one form. If they
are re-used via cut-and-paste, a maintenance nightmare slowly builds.
I prefer an event handler like
Private Sub DBGrid1_UnboundAddData(ByVal RowBuf As MSDBGrid.RowBuffer, _
NewRowBookmark As Variant)
RowBuffer.RowCount = m_oInvoiceEngine.AddItem(RowBuf.Value(0, 0), _
RowBuf.Value(1, 0), _
m_oCustomer, _
NewRowBookmark)
End Sub
Private Sub Button1_Click()
m_oInvoiceFormObject.Clicked
End Sub
Now the form no longer has any references to business objects or database
access; this is all in the other object.
While this is a step in the right direction compared to "typical" fat client
coding techniques, there are some serious flaws.
First, there is the temptation to give InvoiceFormObject a reference to InvoiceForm or one or more controls on the form. Since InvoiceFormObject has business and/or database logic, it is not part of the Presentation layer. Since one of the goals of n-tier architecture is to be able to change the hardware platform on which each tier resides, we would want to be able to move InvoiceFormObject to a remote machine. To do this, we would have to now be able to pass references to the form and/or controls across the network. Even if we technically can, performance certainly can suffer.
This is good general advice: do not pass references to UI objects to the other layers. Instead, pass values from the UI objects to the other layers, and set values of UI objects with values returned from other layers. For example,
Private Sub Button1_Click()
Dim curPrice As Currency
Dim strDesc As String
m_oItemLookup txtItemKey.Text, strDesc, curPrice
txtDesc.Text = strDesc
txtPrice.Text = CStr(curPrice)
End Sub
A second and larger flaw is that the code in InvoiceFormObject may be needed for several forms. We do not want to have code duplicated multiple times through cutting and pasting. Logic related to a particular business process should be written once and shared by all forms that require it.
My personal wish is that environments soon include controls that can be bound to properties of arbitrary object instances, or perhaps even output parameters or return values of methods of said instances. Then the user interface could be declaratively connected to business process or business logic objects. This is a step beyond current Smalltalk and Java environments (Visual Age, PARTS, Visual Cafe) that allow procedural code to be generated for such links in a wizard-like way by interacting with the UI at design time.
Many (but not all) object-oriented software engineering experts that I admire recommend never creating separate classes for processes. They argue that, for example, an instance of a Product class should be able to re-order itself, or that an Invoice should be able to save itself and arrange for picking and shipment.
Well, I have gone this route more than once and never liked where it led me. Simple processes typically worked out fine. More complex processes, however, require the services of many, many different classes. The class chosen to orchestrate the process becomes coupled to many otherwise unrelated classes. Likewise, the class assumes many responsibilities that are more clearly related to the process than the concept being modeled. Using separate classes to represent processes yields lightly coupled classes that model business entities, and heavily coupled classes that model processes. Since processes change more frequently than simple rules or attributes associated with business objects, most of the on-going software maintenance is concentrated on the process classes.
Translated from the lower level of classes to the higher level of architecture service layers, similar advantages apply. Changes to processes affect only the Business services that implement processes. Further, the process services provide a layer of insulation between the UI and the business objects.