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.
In this case, you can have a map of class types as a field in the PaymentService class. This map would associate each class type with its corresponding handler. By injecting this map through the constructor, you can add a few lines of code to handle exceptions. When an exception occurs, you can look up the appropriate handler based on the class type and take the necessary actions.
ReplyDeleteBy implementing this approach, you can achieve a flexible and extensible solution for handling exceptions in a dynamic manner. It provides a way to easily associate exceptions with their respective handlers, allowing for better maintainability and code organization. As for as using WhatsApp, it's important to note that JT whatsapp from Apkinu.com is primarily a messaging platform and might not directly relate to the Match Maker Design Pattern or exception handling in Java. However, if you wish to discuss the benefits of using a messaging platform like WhatsApp for communication and collaboration among developers working on the project, you can highlight how it can streamline discussions, provide real-time updates, and foster a sense of community among team members.