Partitioning Applications in Smalltalk

Separating client and server objects

Jay Almarode

Jay has been programming in Small-talk since 1986 and is currently a senior software engineer at GemStone Systems. He can be contacted at almarode@slc.com.


Smalltalk has historically been used as a client-side-only development language because its initial commercial implementations were single-user systems primarily known for their GUI-building technology. For the most part, multiuser, distributed Smalltalk systems have only been commercially available for a couple of years. Such systems operate on server-class machines and take advantage of shared memory, asynchronous I/O, and transaction logging to provide the throughput required for multiuser, enterprise-wide database applications.

When Smalltalk is used as a client-only language, the application and business-logic objects, as well as the presentation-logic objects, must all reside on the client machine. To share these objects and make them persistent, the typical client Smalltalk application connects to a legacy or relational data server, which stores the state of the objects. This architecture has both technical and business drawbacks. The data servers typically do not have the capability to execute complex business logic. They may provide some query capability or stored procedures, but they do not provide an object model with a computationally complete, extensible language such as Smalltalk. Consequently, much data must be transferred to the Smalltalk client to execute the application logic. As the number of client workstations in an enterprise-wide application increases, the network becomes overloaded. As applications require more data to execute complex business logic, the client machine needs more memory and processing power.

The business logic is a shared resource and a strategic asset to the company. Duplicating business logic across thousands of clients poses security risks, makes maintenance expensive, and discourages frequent updates to the application. It should be under centralized control.

Server-Based Smalltalk

Two new technologies are emerging to address these needs: three-tier architecture and server-based Smalltalk. Three-tier architecture evolved from the client/ server architecture. It defines a middle tier (called an "application server") between multiple client machines and a data server. The application server is where shared business logic is executed. The three-tier architecture reduces the amount of data transferred between the client and the application server, since business logic can be executed on the application server rather than by the client. The application server also provides a central point of control to update business logic, implement security mechanisms, and provide fault tolerance of key data.

Server-based Smalltalk provides the implementation technology to build object application servers. Server-based Smalltalk is a multiuser environment with an execution engine tuned for disk access to handle many large-sized objects. In addition, server-based Smalltalk provides the following:

With a Smalltalk environment optimized for multiuser execution, you can implement shared business objects with the same object-oriented language used to build client applications. This makes partitioning the application easier, since a developer can build an application entirely on the client workstation, then move portions of it to the server as needed. Because the same code can execute on either the client or the server, it is easier to change partitioning decisions to tune the application. With a common object model on the client and the application server, objects do not need to be transformed from one form to another. Relational data is mapped into objects less frequently in the object application server (where only a single network connection to the relational database is required) than on the client.

When partitioning an application, you must determine whether objects should reside on the client or the server. Candidates for server objects include security-sensitive objects, large collections of objects requiring optimized query capability, objects requiring shared access or fault tolerance, business objects, and gateway objects (ones that provide a view of raw data on the data server). Client-side objects include window or GUI, application-specific, and view objects that provide a view of a server object.

An Example Application

To illustrate partitioning, consider an application for a lending institution that must determine risk of loan default. For simplicity, the application will evaluate only loan applicants who have previously borrowed from the lender. In this case, the lender considers the applicant's loan history and other customers with similar assets and liabilities.

In an unpartitioned application, the Smalltalk client relies upon a relational-database server to store the shared customer records. Once all the applicant's information is entered, an operation is invoked to determine if the applicant qualifies for the loan. The Smalltalk code implements the business logic as follows:

    1. Receive the applicant's social-security number, name, and requested loan amount as input.

    2. Compose an SQL query string to determine if the applicant is a previous customer, then transmit the query to the relational server.

    3. Receive the query result back as nested arrays of basic data types (a tabular representation of data in Smalltalk). Map the raw data into a new instance of class Applicant, with additional information from the query result, such as address, phone number, and so on.

    4. If the applicant is a previous customer, compose another SQL query string that retrieves the applicant's previous records. (A clever SQL programmer can get this information bundled with the first SQL query.)

    5. Receive the query result back as nested arrays of basic data types, and map the raw data into instances of class LoanHistory.

    6. Compose an SQL query string that retrieves other customers with similar assets, liabilities, and loan amounts.

    7. Receive the query results back and map to instances of class Customer.

    8. Invoke the analysis code that determines if this applicant is a bad risk.

This architecture has a number of drawbacks. Every time the application is run, tabular relational data must be transformed into objects. If the relational schema is modified, every client Smalltalk application must be updated with new transformation code. To execute the analysis algorithm, all customer data must be transmitted to the client. If many client workstations transmit large amounts of data for loan applicants, the network may become overloaded. Populating the client with a large number of customers to execute the analysis algorithm can stress the memory and CPU capacity of the client machine. Moving the customer data to the client to perform the analysis poses a security risk for sensitive data. Finally, the analysis algorithm is duplicated on every client machine, requiring all clients to be updated if the algorithm changes.

To overcome these drawbacks, I redesigned the application for a three-tier architecture and partitioned the application. I wrote the server code (see Listing One) in GemStone Smalltalk. The client code is written for either VisualWorks, Visual Smalltalk, or VisualAge using the GemStone-Smalltalk Interface; see Listing Two.

The input data (the applicant's name, social-security number, and requested loan amount) belong on the client, as well as the window, form, and widget objects used to prompt and display this data. Since large collections of objects, or objects requiring security or fault tolerance, belong on the server, the set of customers should reside there also. Likewise, business objects and objects requiring shared access belong on the server; therefore, the object(s) that implement the risk-analysis algorithm belong there. Finally, objects that present a view of server objects belong on the client machine. The applicant object is a view of a server object, because not all of the applicant's state is needed in the client (for example, the applicant's loan history remains on the server, but his address is displayed in a client window).

Partitioning Mechanisms

Once the application is partitioned, the client can reference and manipulate server objects in Smalltalk using either forwarders or replicates.

A forwarder can be thought of as a cover for a server object masquerading as a client object. A forwarder does not contain any state of the server object, and when a message is sent to a forwarder, its behavior is executed on the server. The Smalltalk message-sending mechanism allows forwarding of messages automatically (by special handling of the doesNotUnderstand: error), so no special code is required to check for the presence of forwarder objects. When a message is sent to a forwarder, arguments are transformed automatically for execution on the server. There are a number of ways you can get a forwarder to a server object. In most cases by default, the return value of a message sent to a server object is a replicate. In GemStone Smalltalk, you can specify that a forwarder be returned instead by prepending the message with fw. Another way is to send the message beForwarder to a replicate. If you want all instances of a particular client class to be forwarders, you can implement the class method instancesAreForwarders to return true.

A replicate is a copy of a server object that resides on the client. When a message is sent to a replicate, its behavior is executed locally. The GemStone-Smalltalk Interface transparency mechanism keeps the state of the two objects in sync so that the replicate always accurately reflects the state of the server object (based upon the current transaction's viewpoint) and vice versa. To enable this feature in either VisualWorks or VisualAge, you can send the message makeGSTransparent to the class of the replicate. When the client application modifies a replicate, it is automatically marked "dirty" and changes are flushed to the server object at an appropriate time (before server behavior is executed, for example, or when the transaction is committed). When other users modify and commit changes to the server object, the replicate in a client will not be updated until the transaction is committed or aborted. This is called "faulting." Ordinarily, the replicate will not be faulted until it is next accessed. However, it is possible to configure it to be faulted immediately when the transaction begins by implementing a faultPolicy method for the class of the replicate. It is also possible to execute additional application code before or after the replicate is faulted by implementing a preFault or postFault method. Listings One and Two illustrate how to set an immediate fault policy and to trigger the Smalltalk-dependency mechanism after a replicate is faulted. This might be useful if the application wanted a window displaying the replicate to be updated immediately when a new transaction began.

An important consideration when programming with replicates is controlling the replication of "composite objects," or objects with nested subobjects. This is useful because a client application may only need a portion of the state of a server object for a particular application. An application needs to control which instance variables are retrieved and how they are assigned (with replicates or forwarders). If an instance variable is to be assigned a replicate, the application may also want to specify how many levels deep to replicate. To exercise this control in GemStone Smalltalk, you implement the method replicationSpec for the class of the replicant in the client. This method returns nested arrays, each of which indicates the name of the instance variable and how it is to be replicated. Again, Listing Two provides examples of this method, where the name and social-security number of an applicant is always faulted in, the address is faulted in to a minimum of two levels, and the employer is always faulted in as a forwarder (the employer object remains on the server).

When not all of a deeply nested object is faulted into the client, a placeholder object must take its place. This object, called a "stub," maintains knowledge of its corresponding object on the server so that it may replicate the object if necessary. Thus, when a stub is sent a message, it retrieves the object from the server, replaces all references to the stub with the retrieved object, then resends the message. The application code does not have to test for the presence of a stub object--in GemStone Smalltalk, this all happens transparently. Conversely, sometimes it is desirable to turn a replicate into a stub to free up the space used by the replicate and its subobjects. You can do this by sending the message stubYourself to a replicate.

Conclusion

These mechanisms can be utilized in a number of ways to partition and then fine tune an application for maximum performance in a client/server environment. The advent of server-based Smalltalk allows this partitioning and provides new solutions to building high-performance applications in Smalltalk.

Listing One

*******************************************************
*******************************************************
*            On the GemStone (server) side 
*******************************************************
*******************************************************
!--------------------------------------------------------------
! This module consists of the class definitions for the object
! application server implemented in GemStone Smalltalk.
!--------------------------------------------------------------
! begin by defining the classes
run
Object subclass: 'Address'
  instVarNames: #(street city zip)
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: #[ #[#street, String], #[#city, String], #[#zip, Integer] ]
  isInvariant: false.
%
run
Object subclass: 'Company'
  instVarNames: #(name address)
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: #[ #[#name, String], #[#address, Address] ]
  isInvariant: false.
%
run
Object subclass: 'LoanHistory'
  instVarNames: #(amount interestRate date status)
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: #[
    #[#amount, Integer],
    #[#interestRate, Float],
    #[#date, DateTime],
    #[#status, Symbol] ]
  isInvariant: false.
%
run
Set subclass: 'LoanHistorySet'
  instVarNames: #()
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: LoanHistory
  isInvariant: false.
%
run
Object subclass: 'Customer'
  instVarNames: #(name ssn address employer loanHistory)
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: #[
    #[#name, String],
    #[#ssn, Integer],
    #[#address, Address],
    #[#employer, Company],
    #[#loanHistory, LoanHistorySet] ]
  isInvariant: false.
%
run
Set subclass: 'CustomerSet'
  instVarNames: #()
  classVars: #()
  poolDictionaries: #()
  inDictionary: UserGlobals
  constraints: Customer
  isInvariant: false.
%
! automatically generate methods to access the instance variables
run
#[ Address, Company, LoanHistory, Customer ] do: [ :aClass |
  aClass compileAccessingMethodsFor: aClass.instVarNames
]
%
! implement various methods that execute on the server
category: 'Accessing'
method: Customer
getLoanHistory
" Return the receiver's loan history set.  If one does not exist,
create it. "
loanHistory isNil
  ifTrue: [ loanHistory := LoanHistorySet new ].
^ loanHistory
%
category: 'Updating'
method: Customer
addLoanHistory: aLoanHistory
" Add the given loan history object to the reciever's loan history set. "
self getLoanHistory add: aLoanHistory
%
category: 'Qualification'
method: CustomerSet
currentInterestRate
" Return the current interest rate for loans. "
" A fixed rate for purposes of this example "
^ 0.12
%
category: 'Searching'
method: CustomerSet
findCustomerWithSSN: ssn
" Query the receiver to find a customer with the given social security
number. "
" Note: this query syntax allows the use of indexes and fast
lookup mechanisms for large collections "
^ self detect: { :cust | cust.ssn = ssn } ifNone: [ nil ]
%
category: 'Qualification'
method: CustomerSet
qualifyBasedOnPastHistory: customer amount: anAmount
" A previous customer is applying for a new loan.  Determine whether
he/she is a bad risk based upon their past loan history and other
previous customers with similar characteristics.  Return a symbol
indicating the status of the loan request. "
" (This algorithm is left as an exercise for the motivated reader) "
| history |
history := LoanHistory new
  amount: anAmount;
  interestRate: self currentInterestRate;
  date: DateTime now;
  status: #accepted.
customer addLoanHistory: history.
^ #accepted
%
category: 'Qualification'
method: CustomerSet
qualifyNewApplicant: applicant amount: inputLoanRequest
" A new customer is applying for a loan.  Determine whether
he/she is a bad risk based upon assets and liabilities.
Return a symbol indicating the status of the loan request. "
" (This algorithm is left as an exercise for the motivated reader) "
| history |
history := LoanHistory new
  amount: inputLoanRequest;
  interestRate: self currentInterestRate;
  date: DateTime now;
  status: #accepted.
applicant addLoanHistory: history.
^ #accepted
%
! Initialize the set of all customers
run
UserGlobals at: #AllCustomers put: CustomerSet new
%
commit

Listing Two

*******************************************************
*******************************************************
* On the GemStone-Smalltalk Interface (client) side 
*******************************************************
*******************************************************
!--------------------------------------------------------------
! This module consists of the class definitions for the client
! application implemented in VisualWorks
!--------------------------------------------------------------
!
Object subclass: #Applicant
  instanceVariableNames: 'name ssn address employer requestedAmount status '
  classVariableNames: ''
  poolDictionaries: ''
  category: 'nil'!
!Applicant methodsFor: 'replication'!
faultPolicy
    "Cause a replicate to be refreshed immediately 
    when a new transaction begins."
    ^#immediate!
postFault
    "The receiver has just been faulted in from 
    GemStone.  Inform any dependents."
    ^self changed: #faulted!
replicationSpec
    "Return a specification of how instance variables
    should be faulted in."
    ^ super replicationSpec , 
    #(
        (name replicate)
        (ssn replicate)
        (address min 2)
        (employer forwarder)
    )! !
"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
Applicant class
    instanceVariableNames: ''!
!Applicant class methodsFor: 'initialization'!
initialize
" Connect this class with a GemStone class named 'Customer'. "
GSI addGlobalConnector: 
  (GSClassConnector stName: #Applicant gsName: #Customer)
! !
!Applicant class methodsFor: 'analysis'!
qualify: inputName ssn: inputSSN amount: inputLoanRequest
" Create a new applicant for a loan and qualify them.  Return the new
applicant. "
| allCustomers applicant status |
" get a forwarder to the large collection of all customers that
  resides on the server "
allCustomers := (GSI fwat: #AllCustomers).
" send a message to find if the applicant is a customer "
" applicant will be instantiated in the client according to the
  replication spec "
applicant := allCustomers findCustomerWithSSN: inputSSN.
applicant isNil
  ifTrue: [ " applicant is a new customer "
    applicant := self new ssn: inputSSN; name: inputName.
    " execute method on server to qualify a first-time applicant "
    status := allCustomers qualifyNewApplicant: applicant amount: 
                                                             inputLoanRequest.
    " if new applicant was accepted, add them to the set of all customers "
    status = #accepted
      ifTrue: [ allCustomers fwadd: applicant ].
  ]
  ifFalse: [ " applicant has borrowed money from us before "
    " execute method on server to qualify an existing customer "
    status := allCustomers qualifyBasedOnPastHistory: applicant amount: 
                                                             inputLoanRequest.
  ].
applicant requestedAmount: inputLoanRequest; status: status.
^ applicant! !
End Listings