Design networked applications in RMI using the Adapter design pattern

A guide to correctly adapting local Java objects for the Web

If you use Java's Remote Method Invocation (RMI), you know that it's easy to understand and implement: The perfect recipe for creating remote objects and services. In fact, Sun's Java tutorial provides an excellent example for creating your first RMI server and client and for running a remote system. However, I've seen many designs that build upon the Sun example and become unwieldy as new functions are added. Novice programmers often resort to odd naming conventions to distinguish local behavior from remote behavior, and, with the addition of new remote services, the remote class can easily grow into a big mess that performs poorly under remote network traffic.

In this article I'll demonstrate how the Adapter pattern, one of the most common design patterns, can be used to adapt a local class to a remote class. Using this design technique, it's easy to distinguish local from remote behavior, and you'll be able to continue to use the local class for your non-networked applications. Also, as the number of remote services grow, it's easy to see which class must handle the request and which objects must be synchronized. Finally, with this technique, your design will be easier to understand and more amenable to changes and fixes from multiple developers.

What is the Adapter pattern?

The Adapter pattern is one of the structural patterns listed in the reference book Design Patterns by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). The Adapter pattern is very common not only to remote client/server programming but also to any situation in which you have one class that you wish to reuse, but the application interface doesn't match the class interface. For this reason, you use an adapter or wrapper to convert the application interface to the existing class interface. In software development, an adapter simply maps the interface of one class to that of another. Adapters are used continually throughout the software development process, hence the term Adapter pattern.

Figure 1, below, illustrates how an adapter works. In this diagram, Client wants to invoke the method request() in the Target interface. Since the Adaptee class has no request() method, it is the job of Adapter to convert the request to an available matching method. Here the Adapter converts the method request() call into the Adaptee method specificRequest() call. The Adapter does this conversion for each method that needs adapting, also known as wrappering.

Figure 1. Adapting an interface to an Adaptee class

This type of adapting is exactly what one needs when creating remote services for the Web. Think of the design process as taking a working local class and publishing its services for the Web. You publish the services not by adding to the local class, but by adapting the local class to the remote interface. In the following sections I'll show you how this is done.

Designing a networked application

As stated in the previous section, classes are best made Web-ready by adapting the local methods to a remote interface. Sun's Java tutorial tells us we have to follow certain steps to make RMI work. These steps are as follows:

  1. Create a remote interface that extends the java.rmi.Remote interface

  2. Provide an implementation of the remote interface

  3. Invoke the remote method calls from the remote client

Serendipitously, these steps closely match the function of the Adapter pattern. The Adapter pattern is initiated when a remote client intends to use a method published in the remote interface. The remote interface extends the tagging interface java.rmi.Remote to let RMI know this interface implements RMI. Rather than forcing the local class to implement the remote method, which creates excessive class bloating and hurts performance and readability, one creates a remote adapter that services a remote method by invoking a similar method in the local class. The local class is unchanged and unaware that it's being adapted to service network requests. Figure 2 shows the relationships of the classes as you apply the Adapter pattern to RMI.

Figure 2. Adapting a Remote interface to a local class

Conceptually, we've created a picture of how we want to use the Adapter pattern in a Java RMI application. Now let's create a concrete implementation of this design. Let's assume we want to implement a remote collection that many remote clients can query or add and delete elements from over a network. For this example, I'll use the Hashtable class in the java.util package, which many Java programmers have used over and over again.

Design the local classes

The first step toward creating a remote collection is to design a local class that implements some function. Conveniently, Sun has created the java.util.Hashtable for us, and we'll use the class as-is with no extra function. I've omitted the synchronized keyword from the methods, but I will reintroduce it later when I discuss how to make the object thread-safe. For your convenience, Listing 1 contains the pertinent methods of the Hashtable class.

int size();
boolean isEmpty();
boolean contains( Object value );
boolean containsKey( Object key );
Object get( Object key );
Object put( Object key, Object value );
Object remove( Object key );
void clear();
Enumeration keys();
Enumeration elements();

Listing 1. The java.util.Hashtable class methods

Design the remote interface

The second step in this exercise is to create a remote interface that takes the services the remote class is offering and publishes them over the network. The interface RemoteMap, shown below, is based on the java.util.Map collection interface, which the hashtable implements in Java 2. Collectively, these methods form the interface that remote clients may call on our networked hashtable.

public interface RemoteMap extends java.rmi.Remote {
   // Constants
   public static final String SERVICENAME = "RemoteMap";
   public int size() throws RemoteException;
   public boolean isEmpty() throws RemoteException;
   public boolean containsKey( Object key ) throws RemoteException;
   public boolean containsValue( Object value ) throws RemoteException;
   public Object get( Object key ) throws RemoteException;
   public Object put( Object key, Object value ) throws RemoteException;
   public Object remove( Object key ) throws RemoteException;
   public void clear() throws RemoteException;
}

Listing 2. A RemoteMap interface: Our remote methods

There are a few details worth pointing out. To start with, all of the remote methods throw a java.rmi.RemoteException. This is nothing to worry about; it's simply a requirement of Java RMI. Also, the RemoteMap interface extends the java.rmi.Remote interface. This is a tagging interface that tells the Java rmic utility which interface is going to be shared over the network; this is another requirement of RMI. I've left out a few of the Map interface methods, such as keySet and valueSet, but these are easy to implement using the hashtable keys and elements enumerations. Finally, there is one constant in the interface -- the string SERVICENAME. This constant will be handy when servers publish an RMI service and clients query the RMI registry to find the name of our public services.

So, now we have a local object and a remote interface. We're now ready to adapt the local class to the remote interface.

Pitfalls of adaptation

Many designers fail to properly execute the next step, adapting the local class to the remote interface. It's tempting to create a new class that extends the java.util.Hashtable class and implements the RemoteMap interface (for example, RemoteHashtable). This is a bad programming choice for several reasons:

  • It's preferable to design using composition rather than inheritance. Why? Because design by inheritance is normally used for adding new methods and attributes to an existing class. Here, we're simply adapting local methods to become remote methods, so design by composition and delegation is more appropriate.

  • Designing with inheritance uses up the single inheritance parent class. Since Java only allows single inheritance, this gives us the dubious choice of extending java.rmi.server.UnicastRemoteObject, as required by RMI, or extending java.util.Hashtable, as required when designing by inheritance. It's preferable to forgo design by inheritance and implement the RMI function.

  • Some local classes are final and may not be extended. For example, this design exercise won't work with the java.util.Vector class because that class is final. If a class is final, you cannot use design by inheritance.

  • Mixing local and remote functions overly complicates the class. This is especially problematic when the remote interface and the local class have similar method names. I've seen supposed solutions where programmers have changed the method names to help prevent this problem -- for example, sizeLocal and sizeRemote. This is ugly! Not to mention that two simple, single-function classes now become one big multifunction class. Even uglier!

To show you how easy it is to create an adapter, and how simple is its function, Listing 3 offers the entire code. Java RMI handles the marshalling and unmarshalling of all call parameters and return values as long as these objects are serializable and implement java.io.Serializable. So, you see, the adapter really is a very simple class, and we haven't made one change to our local implementation, the hashtable.

public class RemoteMapAdapter extends UnicastRemoteObject
   implements RemoteMap {
   // Constructors
   // The owner publishes this service in the RMI registry.
   public RemoteMapAdapter( Hashtable adaptee ) throws RemoteException {
      this.adaptee = adaptee;
   }
   // RemoteMap role
   public int size() throws RemoteException {
      return adaptee.size(); }
   public boolean isEmpty() throws RemoteException {
      return adaptee.isEmpty(); }
   public boolean containsKey( Object key ) throws RemoteException {
      return adaptee.containsKey( key ); }
   public boolean containsValue( Object value ) throws RemoteException {
      return adaptee.contains( value ); }
   public Object get( Object key ) throws RemoteException {
      return adaptee.get( key ); }
   public Object put( Object key, Object value ) throws RemoteException {
      return adaptee.put( key, value ); }
   public Object remove( Object key ) throws RemoteException {
      return adaptee.remove( key ); }
   public void clear() throws RemoteException {
      adaptee.clear(); }
   // Fields
   protected Hashtable adaptee;
}

Listing 3. A RemoteMapAdapter, adapting RemoteMap to Hashtable

We've finished the most important part of this example -- we've finished writing our adapter class. Let's wrap up a few more loose ends, and we'll be done.

Final touches

To finish the exercise, we must do a few more things. First, we must compile the interface and the adapter given in the previous two sections. Second, we must create the stubs and skeletons required to run the RemoteMapAdapter over a network. This is done in Java using the rmic tool. To run rmic on the RemoteMapAdapter class, type: rmic RemoteMapAdapter.

Next, we need two simple programs to run this example -- a server and a client. First, we implement a remote server that creates a local hashtable and publishes the remote version using the adapter we've designed. The important bits are shown below. Unlike many examples I've seen, it's possible to run a Java RMI server from within a program and not from the command line. The createRegistry method performs this step. Then, we create both the local hashtable and the adapter that performs the remote publishing work. Finally, the RMI registry publishes the RemoteMapAdapter using a string name and the rebind method of the local registry. Voila, the server is running and ready to handle remote clients! See Listing 4.

   ...
   try {
      System.out.println( "RemoteMapServer creating a local RMI registry on
 the default port." );
      Registry localRegistry = LocateRegistry.createRegistry(
Registry.REGISTRY_PORT );
      System.out.println( "RemoteMapServer creating local object and remote
 adapter." );
      adaptee = new Hashtable();
      adapter = new RemoteMapAdapter( adaptee );
      System.out.println( "RemoteMapServer publishing service \"" +
RemoteMap.SERVICENAME + "\" in local registry." );
      localRegistry.rebind( RemoteMap.SERVICENAME, adapter );
      System.out.println( "Published RemoteMap as service \"" +
RemoteMap.SERVICENAME + "\". Ready." );
   } catch (RemoteException e) {
      System.out.println( "RemoteMapServer problem with remote object,
exception:\n   " + e );
   }
   ...

Listing 4. A RemoteMapServer that publishes a RemoteMapAdapter

The last step is to create a few clients that can add, query, and remove objects remotely. Listing 5 shows how the client finds the remote registry on the server and queries the service offered via the given service name. Notice that the registry lookup method returns the RemoteMap interface and not a RemoteMapAdapter object. This is true of all RMI services because many types of remote objects may implement the remote interface.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more