Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generalize a method call in Java (to avoid code duplication)

I have a process that needs to call a method and return its value. However, there are several different methods that this process may need to call, depending on the situation. If I could pass the method and its arguments to the process (like in Python), then this would be no problem. However, I don't know of any way to do this in Java.

Here's a concrete example. (This example uses Apache ZooKeeper, but you don't need to know anything about ZooKeeper to understand the example.)

The ZooKeeper object has several methods that will fail if the network goes down. In this case, I always want to retry the method. To make this easy, I made a "BetterZooKeeper" class that inherits the ZooKeeper class, and all of its methods automatically retry on failure.

This is what the code looked like:

public class BetterZooKeeper extends ZooKeeper {

  private void waitForReconnect() {
    // logic
  }

  @Override
  public Stat exists(String path, Watcher watcher) {
    while (true) {
      try {
        return super.exists(path, watcher);
      } catch (KeeperException e) {
        // We will retry.
      }
      waitForReconnect();
    }
  }

  @Override
  public byte[] getData(String path, boolean watch, Stat stat) {
    while (true) {
      try {
        return super.getData(path, watch, stat);
      } catch (KeeperException e) {
        // We will retry.
      }
      waitForReconnect();
    }
  }

  @Override
  public void delete(String path, int version) {
    while (true) {
      try {
        super.delete(path, version);
        return;
      } catch (KeeperException e) {
        // We will retry.
      }
      waitForReconnect();
    }
  }
}

(In the actual program there is much more logic and many more methods that I took out of the example for simplicity.)

We can see that I'm using the same retry logic, but the arguments, method call, and return type are all different for each of the methods.

Here's what I did to eliminate the duplication of code:

public class BetterZooKeeper extends ZooKeeper {

  private void waitForReconnect() {
    // logic
  }

  @Override
  public Stat exists(final String path, final Watcher watcher) {
    return new RetryableZooKeeperAction<Stat>() {
      @Override
      public Stat action() {
        return BetterZooKeeper.super.exists(path, watcher);
      }
    }.run();
  }

  @Override
  public byte[] getData(final String path, final boolean watch, final Stat stat) {
    return new RetryableZooKeeperAction<byte[]>() {
      @Override
      public byte[] action() {
        return BetterZooKeeper.super.getData(path, watch, stat);
      }
    }.run();
  }

  @Override
  public void delete(final String path, final int version) {
    new RetryableZooKeeperAction<Object>() {
      @Override
      public Object action() {
        BetterZooKeeper.super.delete(path, version);
        return null;
      }
    }.run();
    return;
  }

  private abstract class RetryableZooKeeperAction<T> {

    public abstract T action();

    public final T run() {
      while (true) {
        try {
          return action();
        } catch (KeeperException e) {
          // We will retry.
        }
        waitForReconnect();
      }
    }
  }
}

The RetryableZooKeeperAction is parameterized with the return type of the function. The run() method holds the retry logic, and the action() method is a placeholder for whichever ZooKeeper method needs to be run. Each of the public methods of BetterZooKeeper instantiates an anonymous inner class that is a subclass of the RetryableZooKeeperAction inner class, and it overrides the action() method. The local variables are (strangely enough) implicitly passed to the action() method, which is possible because they are final.

In the end, this approach does work and it does eliminate the duplication of the retry logic. However, it has two major drawbacks: (1) it creates a new object every time a method is called, and (2) it's ugly and hardly readable. Also I had to workaround the 'delete' method which has a void return value.

So, here is my question: is there a better way to do this in Java? This can't be a totally uncommon task, and other languages (like Python) make it easier by allowing methods to be passed. I suspect there might be a way to do this through reflection, but I haven't been able to wrap my head around it.

like image 628
dln385 Avatar asked May 12 '26 08:05

dln385


1 Answers

This seems (at least to me) to be the "right" Java-esque (or whatever the analog to "Pythonic" is in Java) refactoring. You have properly used the Template Method pattern and your passing of final variables into the inner anonymous subclasses is spot-on and just as the designers of inner classes intended. You have preserved static typing and are using generics well.

A solution using reflection is indeed possible, but it does sacrifice some static typing niceties and your code will have to catch a few checked exceptions that come with invoking methods, adding some clutter. Reflection is overrated and IMHO not necessary here. We tend to use that in Java more for instrumentation rather than making code easier to read. Wait for Java 8 if you want to pass methods cleanly!

Also, I do not believe your code is unreadable. Professional Java programmers should be able to read this. Inner classes exist for this type of thing. That said, it is possible to refactor "one more step" and make what are now anonymous classes be named local classes (stil within your methods of course) so your invocation of run() would not be so hard to find. Other than that I have no real refactoring suggestions.

like image 103
Ray Toal Avatar answered May 14 '26 21:05

Ray Toal



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!