2015/10/12

Static vs dynamic method overloading with Java and Groovy

Groovy code may be quite similar to Java at the first glimpse. This may sometimes lead to caveats. A good example of such is method overloading by an argument type. Code may look the same in both languages but it's going to work different.

Example

Consider the following class:

public class Foo {

   public String bar(Object value) {
      return "Object: " + value;
   }

   public String bar(String value) {
      return "String: " + value;
   }

   public String bar(Integer value) {
      return "Integer: " + value;
   }
}

Although it's a Java class it doesn't really matter at this point whether it's Groovy or Java. The caller class matters.

JAVA: static binding for overloaded methods

Caller class written in Java:
public class StaticBindingExample {

   public static void main(String[] args) {
      Object number = 44;
      Object text = "plop!";

      Foo foo = new Foo();

      foo.bar(number);   // returns "Object: 44"
      foo.bar(text);     // returns "Object: plop!"
   }

}
By Java static nature, calling the bar() method always invokes the one which signature matches the argument declared type.

Groovy: dynamic binding for method overloading

That's the exact same code for caller class as in the previous snippet just written in Groovy:
class DynamicBindingExample {

   static void main(String[] args) {
      Object number = 44
      Object text = 'plop!'

      Foo foo = new Foo()

      foo.bar(number)  // returns "Integer: 44"
      foo.bar(text)    // returns "String: plop!"
   }
}
Here we can see the difference. Despite the arguments for the method call were declared as Object, Groovy dynamic type evaluation always tries to match the closest matching method at runtime. Thus methods relevant for the actual argument type were executed.

Advantage of dynamic binding

Please consider an example of some payment service written in Java:
public class PaymentService {

   private final CustomerRepository customerRepository;
   private final AccountService accountService;

   public PaymentService(CustomerRepository customerRepository, AccountService accountService) {
      this.customerRepository = customerRepository;
      this.accountService = accountService;
   }

   public void pay(Integer customerId, BigDecimal amount) {
      Customer customer = customerRepository.findById(customerId);   // may throw UnknownCustomerException
      accountService.substract(customer, amount);                    // may throw InsufficientFundsException
   }
}
How is going to look exception handling if we add try-catch block around business logic within the pay() method? Well, quite typical:
public void pay(Integer customerId, BigDecimal amount) {
   try {
      Customer customer = customerRepository.findById(customerId);   // may throw UnknownCustomerException
      accountService.substract(customer, amount);                    // may throw InsufficientFundsException
   } catch (UnknownCustomerException ex) {
      handle(ex);
   } catch (InsufficientFundsException ex) {
      handle(ex);
   } catch (Exception ex) {
      handle(ex);
   }
}

private void handle(UnknownCustomerException ex) {
   // relevant logic for handling unknown customer
}

private void handle(InsufficientFundsException ex) {
   // relevant logic for handling insufficient funds
}

private void handle(Exception ex) {
   // relevant logic for handling unexpected exception
}
Obviously if the handling logic isn't complex, delegation to separate methods may be skipped in favour of in-line handling inside of each catch block. Although splitting the logic into methods or encapsulating exception handling in injected collaborator is usually a better way and cleaner separation of concerns.

This way or the other we may see straight away that catch blocks are rather redundant in this situation. How would it look like with Groovy's dynamic overloading? It’s enough to have single, generic type, catch block. Invocation is routed to appropriate handle() method by the argument type anyway:
void pay(Integer customerId, BigDecimal amount) {
   try {
      Customer customer = customerRepository.findById(customerId)   // may throw UnknownCustomerException
      accountService.substract(customer, amount)                    // may throw InsufficientFundsException
   } catch (Exception ex) {
      handle(ex)
   }
}

private void handle(UnknownCustomerException ex) {
   // relevant logic for handling unknown customer
}

private void handle(InsufficientFundsException ex) {
   // relevant logic for handling insufficient funds
}

private void handle(Exception ex) {
   // relevant logic for handling unexpected exception
}
The same implemented with the less amount of a cleaner code? That's what a craftsman appreciates.

The same behaviour with Java static overloading

There is a way to achieve "the same" with pure Java, the Match Maker Design Pattern. I'm not sure about the name of the pattern itself though. I've got the feeling that Martin Fowler, Gang of Four or some other guru might have come with a better definition for such a case. I can't find it at the moment so let’s get back to the code (you're welcome to comment if you know it though).

Simply we may have a "routing" map of class type to be handled to the handler for it. In our case it can be done by adding mentioned map as the PaymentService class field and then using it in the catch block as follows. Let say the handlers map is injected via constructor then we simple have a few more lines of code:
public class PaymentService {
   // …
   private final Map<Class<? extends Exception>, ExceptionHandler> handlers;

   public PaymentService(CustomerRepository customerRepository, AccountService accountService,
                         Map<Class<? extends Exception>, ExceptionHandler> handlers) {
      // … 
      this.handlers = handlers;
   }

   public void pay(Integer customerId, BigDecimal amount) {
      try {
         // …
      } catch (Exception ex) {
         ExceptionHandler handler = handlers.get(ex.getClass());
         handler.handle(ex);
      }
   }
}
Obviously we need the handler interface as well, nothing surprising here:
interface ExceptionHandler {
   public void handle(Exception ex)
}
That's it, isn't it? Well, not quite, to be honest. To get the proper impression of how much more code actually is necessary the best way is to show the complete example. If we were about to encapsulate the same, full logic within a single class it would look like:
public class PaymentService {

   private final CustomerRepository customerRepository;
   private final AccountService accountService;
   private final Map<Class<? extends Exception>, ExceptionHandler> handlers;

   public PaymentService(CustomerRepository customerRepository, AccountService accountService,
                         Map<Class<? extends Exception>, ExceptionHandler> handlers) {
      this.customerRepository = customerRepository;
      this.accountService = accountService;
      this.handlers = createExceptionHandlers();
   }

   public void pay(Integer customerId, BigDecimal amount) {
      try {
         Customer customer = customerRepository.findById(customerId);   // may throw UnknownCustomerException
         accountService.substract(customer, amount);                    // may throw InsufficientFundsException
      } catch (Exception ex) {
         ExceptionHandler handler = handlers.get(ex.getClass());
         handler.handle(ex);
      }
   }

   private Map<Class<? extends Exception>, ExceptionHandler> createExceptionHandlers() {
      HashMap<Class<? extends Exception>, ExceptionHandler> handlers = new HashMap<>();
      handlers.put(UnknownCustomerException.class, createUnknownCustomerExceptionHandler());
      handlers.put(InsufficientFundsException.class, createInsufficientFundsExceptionHandler());
      handlers.put(Exception.class, createUnexpectedExceptionHandler());
      return handlers;
   }

   private ExceptionHandler createUnknownCustomerExceptionHandler() {
      return new ExceptionHandler() {
         @Override
         void handle(Exception ex) {
            // relevant logic for handling unknown customer
         }
      };
   }

   private ExceptionHandler createInsufficientFundsExceptionHandler() {
      return new ExceptionHandler() {
         @Override
         void handle(Exception ex) {
            // relevant logic for handling insufficient funds
         }
      };
   }

   private ExceptionHandler createUnexpectedExceptionHandler() {
      return new ExceptionHandler() {
         @Override
         void handle(Exception ex) {
            // relevant logic for handling unexpected exception
         }
      };
   }

}

Summary

Clearly solution complexity may grow really fast if one wants to mimic dynamic binding behaviour in a language which by its nature does it the static way. Dynamic method overloading comes then as really helpful thing which allows avoiding unnecessary clutter in the code.

On the other hand it suits well rather simpler scenarios of dealing with objects from the same inheritance tree. It doesn't have to always be the best approach though. Too much logic placed within a single class is almost never a good idea. As usual the trick is to choose the proper solution for the job as well as the programming language itself.

No comments:

Post a Comment