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.