A simple but dramatic example of the benefits of application partitioning is to run two identical queries on a collection. One query runs on the server and returns only the result of the query; the second query runs on the client, requiring the collection to be copied to the client.[3]
[3] This example is based on a demonstration originally created by GemStone to show the benefits of application partitioning using their application server.
It's pretty obvious that since the only difference between the two queries is the amount of data being copied across the network, the second query that copies much more data is slower. For the example, I use a large array of strings and create a query that returns that subset of strings that includes the query string, e.g., "el" is included in "hello" but not in "hi."
The query method is straightforward:
public static String[ ] getQuery(String obj, String[ ] array) { Vector v = new Vector( ); for (int i = 0; i < array.length; i++) if (array[i].indexOf(obj) != -1) v.addElement(array[i]); String[ ] result = new String[v.size( )]; for (int i = 0; i < result.length; i++) result[i] = (String) v.elementAt(i); return result; }
To run the query as a server method, I declare one server method in a server object (i.e., in the ServerObject interface):
public String[ ] getServerQuery(String obj);
This is also straightforward. The client calls getServerQuery( ) on the server proxy object and receives the results. To run the query on the client, I declare a method (again in the ServerObject interface) giving access to the String array containing the strings to be compared:
public String[ ] getQueryArray( );
The server implementation of getServerQuery( ) is simple (declared in the class that implements the ServerObject interface):
public String[ ]getServerQuery(String obj) { return getQuery(obj, getQueryArray( )); }
The client query implementation is similarly straightforward (this could be declared in any client class that has access to the proxy object, including the stub class[4]):
[4] The client query method is logically defined in the client stub or the client proxy object defined for the application. But technically, it is not forced to be defined in these classes and can be defined in any client class that has access to the server proxy object.
public String[ ] getClientQuery(ServerObject serverProxy, String obj) { return getQuery(obj, serverProxy.getQueryArray( )); }
In fact, there isn't much difference between the two method definitions. But when a test is run to compare the two different queries, the results are startling. For my test, I used an array of 87,880 four-letter strings. The test result produced five strings. Using RMI, the client query took 35 times longer than the server query, and required the transfer of over 600,000 bytes compared to under 100 bytes for the server query. In absolute times, the server query gave a reasonable response time of well under a second. The client query produced an unacceptable response time of over 15 seconds, which would have users wondering what could possibly be taking so long.
Application partitioning similarly applies to moving some of the "intelligence" of the server to the client to reduce messaging to the server. A simple example is a client form where various fields need to be filled in. Often, some of the fields need to be validated according to data format or ranges. For example, a date field has a particular format, and the parts of the date field must fall in certain ranges (e.g., months from 1 to 12). Any such validation logic should be executed on the client; otherwise, you are generating a lot of unnecessary network transfers. The example of date-field validation is perhaps too obvious. Most applications have a widget customized to handle their date-field entries. But the general area of user-interface presentation logic is one in which the logic should reside mostly on the client.