ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Learning Command Objects and RMI
Pages: 1, 2, 3, 4

There's Another Problem Here

Beyond the sheer density of the above code, this solution creates other problems. RMI tries very hard to make remote method calls look a lot like ordinary method calls. This is convenient, and makes RMI very easy to use. But it also leads to mental lapses on the part of programmers -- they slip, and forget that the method call is really a remote call. Consider the first version of the remote call code.



  private class TranslationListener implements ActionListener
    public void actionPerformed(ActionEvent actionEvent) {
      String resultText = "";
      Translator translator = _translatorPanel.getTranslator();
      Word word = _wordPanel.getWord();
      Language targetLanguage = _translatorPanel.getTargetLanguage();

      try {
        Word result = (Word) translator.translate(word, targetLanguage);
        resultText = result.toString();
      }
      catch (Exception anyException) {
        resultText = anyException.toString();
      }
      finally {
        _resultsPanel.setText(resultText);
      }
    }

  }

A programmer who reads the above code has absolutely no way to deduce from the code that a remote method call is involved (and, hence, a programmer reading through the source code three months later probably won't suspect that a retry loop is necessary). This means that if there are 37 places in your code that involve remote calls, you can expect a distribution roughly like the following:

  • 20 implementations involving a retry loop and a time delay. Of course, there will be many slight implementation differences ("This remote call retries five times, with a wait of four seconds between retries" .... "This remote call retries three times, with waits of five, 10, and 15 seconds") with no overarching communication strategy and very little code reuse between the listeners.

  • Ten semi-correct implementations (with a retry loop but without delays, or where the retry logic isn't quite correct).

  • Seven places where the original 10-line try/catch/finally logic made it into the production code (and wasn't caught by QA because QA is on a nice network with a fast connection to the server).

That is, you can expect lots of code redundancy and more than a few places where the code either isn't very good or isn't correct.

Command Objects

The AbstractRemoteMethodCall and RetryStrategy classes

The solution to the problems outlined in the previous section is to implement, and consistently use, command objects. A command object is, quite simply, an object that encapsulates a method call. That is, to the object which is using the command object, it's simply a fancy method invocation. For example, our code will eventually replace the following three lines from our first pass at writing the code:

try {
  Word result = (Word) translator.translate(word, targetLanguage);
  resultText = result.toString();
}

Here's the new code:

TranslateWord translateMethod = new TranslateWord(translator, word, targetLanguage);
try {
  Word result = (Word) translateMethod.makeCall();
  resultText = result.toString();
}

This new code simply adds a level of indirection by creating an instance of the TranslateWord command object (which encapsulates the original method call) and then invoking its makeCall method.

Why would we do such a thing ? Well, Design Patterns (Gamma, etc., Addison-Wesley, 1995) gives the following motivation for the command object pattern:

Intent. Encapsulates a request as an object, thereby letting you parametrize clients with different requests, queue or log requests, and support undoable operations.

These are all really good reasons for using the comand object pattern: they're compelling, easy to understand, and apply to a wide range of programs. (Every program with a graphical user interface should support some notion of "undo." Command objects are the canonical way to do so.)

In effect, we're adding a fourth reason, which really only applies to distributed programming, to the above list. Namely, you might also want to use command objects if the request might fail for transient reasons. In such cases, it's easy to implement retry logic as part a generic base class.

Note: In the second article in this series, we'll add a fifth reason (to refresh or resynchronize local caches of information acquired from distinct processes before processing a request).

The solution I'm going to outline here is one I've used on several projects in order to wrap remote method calls used in server-to-server communication. It involves one abstract base class, AbstractRemoteMethodCall, one abstract convenience class, RetryStrategy, and two concrete subclasses of RetryStrategy. These classes play the following roles.

  • RetryStrategy. This is basically an object wrapping an integer (the current number of tries). It tracks the number of attempts that have been made and implements the waiting strategy. In the code for these articles, I've made this into an abstract base class and implemented two concrete subclasses: AdditiveWaitRetryStrategy and ExponentialBackoffRetryStrategy.

  • AbstractRemoteMethodCall. This class encapsulates the retry logic in a single method named makeCall. In order to do this in a flexible way, it has a number of template methods, such as handleRetryException, which have default behaviors but which can be overridden by subclasses.

Here's the code for RetryStrategy and AdditiveWaitRetryStrategy.

public abstract class RetryStrategy {
  public static final int DEFAULT_NUMBER_OF_RETRIES = 3;
  private int _numberOfTriesLeft;

  public RetryStrategy() {
    this(DEFAULT_NUMBER_OF_RETRIES);
  }

  public RetryStrategy(int numberOfRetries){
    _numberOfTriesLeft = numberOfRetries;
  }

  public boolean shouldRetry() {
    return (0 < _numberOfTriesLeft);

  }

  public void remoteExceptionOccured() throws RetryException {
    _numberOfTriesLeft --;
    if (!shouldRetry()) {
      throw new RetryException();
    }
    waitUntilNextTry();
  }

  protected abstract long getTimeToWait();

  private void waitUntilNextTry() {
    long timeToWait = getTimeToWait();

    try {
      Thread.sleep(timeToWait );
    }
    catch (InterruptedException ignored) {}
  }
}

public class AdditiveWaitRetryStrategy extends RetryStrategy {
  public static final long STARTING_WAIT_TIME = 3000;

  public static final long WAIT_TIME_INCREMENT = 5000;


  private long _currentTimeToWait;
  private long _waitTimeIncrement;
  
  public AdditiveWaitRetryStrategy () {
    this(DEFAULT_NUMBER_OF_RETRIES , STARTING_WAIT_TIME, WAIT_TIME_INCREMENT);
  }

  public AdditiveWaitRetryStrategy (int numberOfRetries, long startingWaitTime, long waitTimeIncrement) {
    super(numberOfRetries);
    _currentTimeToWait = startingWaitTime;

    _waitTimeIncrement = waitTimeIncrement;

  }

  protected long getTimeToWait() {
    long returnValue = _currentTimeToWait;

    _currentTimeToWait += _waitTimeIncrement;

    return returnValue;
  }
}

The version of AbstractRemoteMethodCall used in this article is only slightly more complex than the code involved in RetryStrategy . Here it is:

import java.rmi.*;
import java.rmi.server.*;

public abstract class AbstractRemoteMethodCall
{
  public Object makeCall() throws ServerUnavailable, Exception {
    RetryStrategy strategy = getRetryStrategy();
    while (strategy.shouldRetry()) {
      Remote remoteObject = getRemoteObject();
      if (null==remoteObject) {
        throw new ServerUnavailable();
      }
      try {
        return performRemoteCall(remoteObject);
      }
      catch (RemoteException remoteException)
      {
        try {
          strategy.remoteExceptionOccured();
        }
        catch (RetryException retryException) {
          handleRetryException(remoteObject);
        }
      }
    }  
    return null;
  }

  protected abstract Remote getRemoteObject() throws ServerUnavailable;

  protected abstract Object performRemoteCall(Remote remoteObject) throws RemoteException, Exception;


  protected RetryStrategy getRetryStrategy() {
    return new AdditiveWaitRetryStrategy();
  }

  protected void handleRetryException(Remote remoteObject) throws ServerUnavailable {
    ExceptionLog.reportException("Repeated attempts to communicate with " + remoteObject + " failed.");
    throw new ServerUnavailable();
  }
}

Essentially, the makeCall method in AbstractRemoteMethodCall embodies the loop we wrote in section two, using four template methods to allow the code to be customized for a particular remote call. This is a fairly clean and straightforward use of the command object pattern.

There is one fly in the ointment: we've lost a fair amount of type information. makeCall is declared as returning instances of Object and throwing instances of Exception. There's no way around the fact that this is ugly -- it means our code will have to perform a cast on the value returned from makeCall and will use a try/catch block that catches instances of Exception. On the bright side, however, catch blocks rely on run-time type information rather than the statically declared exception types. Thus, for example, if we write code like this:

try {
  Word result = (Word) translateMethod.makeCall();
  resultText = result.toString();
}
catch (CouldNotTranslateException couldNotTranslateException) {
  resultText = COULD_NOT_TRANSLATE_STRING;
}
catch (Exception e) {
  resultText = e.toString();
}
finally {
  _resultsPanel.setText(resultText);
}

then if the server throws an instance of CouldNotTranslateException, _resultTextwill be set to COULD_NOT_TRANSLATE_STRING (even though makeCall isn't declared as throwing CouldNotTranslateException, the run-time type of the exception determines which catch block is actually used).

Pages: 1, 2, 3, 4

Next Pagearrow