6 tips to writing loosely coupled Java applications with examples
Q. What is tight coupling?
A. If class OrderServiceImpl relies on parts of class PaymentServiceImpl that are not part of class PaymentServiceImpl's interface PaymentService, then the OrderServiceImpl and PaymentServiceImpl are said to be tightly coupled. In other words, OrderServiceImpl knows more than what it should about the way in which PaymentServiceImpl was implemented. If you want to change PaymentServiceImpl with a separate implementations BasicPaymentServiceImpl, then you need to modify the OrderServiceImpl class as well by changing PaymentServiceImpl to BasicPaymentServiceImpl
Tip #1: Coding to interface will loosely couple classes.
Q. What is loose coupling?
A. If the only knowledge that class OrderServiceImpl has about class PaymentServiceImpl, is what class PaymentServiceImpl has exposed through its interface PaymentService, then class OrderServiceImpl and class PaymentServiceImpl are said to be loosely coupled. If you want to change PaymentServiceImpl with a separate implementations BasicPaymentServiceImpl, then you don't need to modify OrderServiceImpl. Change only OrderServiceMain from
PaymentService payService = new PaymentServiceImpl();
to
PaymentService payService = new BasicPaymentServiceImpl();
This is what the Dependency Inversion Principle (DIP) states.
Q. Can the above classes be further improved in terms of coupling?
A. Yes. Change the PaymentService method signature from
public abstract void handlePay(int accountNumber, BigDecimal amount);
to
public abstract void handlePay(PaymentDetail paymentDetail);
Tip #2: Design method signatures carefully by avoiding long parameter lists. As a rule, three parameters should be viewed as a practical maximum, and fewer is better (as recommended by Mr. Joshua Bloch.). This is not only from coupling perspective, but also in terms of readability and maintainability of your code.
It is likely that the PaymentService may need more parameters than account number and amount to process the payment. Every time you need to add a new parameter, your PaymentService interface method signature will change, and all other classes like OrderService that depends on PaymentService has to change as well to change its arguments to passed. But, if you create a value object like PaymentDetail, the method signature does not have to change. You add the new field to the PaymentDetail class.
Tip #3: Design patterns promote looser coupling
The PaymentService will not only be used by the OrdersServiceMain, but can be used by other classes like RequestServiceMain, CancelServiceMain, etc. So, if you want to change the actual implementation of PaymentService between PaymentServiceImpl and BasicPaymentServiceImpl without having to change OrdersServiceMain, RequestServiceMain, and CancelServiceMain, you can use the factory design pattern as shown by the PaymentFactory class. You only have to make a change to the PaymentFactory class to return the right PaymentService implementation.
package com.coupling; import java.math.BigDecimal; public class OrderServiceMain { public static void main(String[] args) { //loosely coupled as it knows only about the factory PaymentService payService = PaymentFactory.getPaymentService(); OrderService orderService = new OrderServiceImpl(payService); orderService.process(12345, BigDecimal.valueOf(250.00)); } }
package com.coupling; public final class PaymentFactory { private static PaymentService instance = null; private PaymentFactory(){} public static PaymentService getPaymentService(){ if(instance == null){ instance = new PaymentServiceImpl(); } return instance; } }
Tip #4: Using Inversion of Control (IoC) Containers like Spring, Guice, etc.
Dependency Injection (DI) is a pattern of injecting a class’s dependencies into it at run time. This is achieved by defining the dependencies as interfaces, and then injecting in a concrete class implementing that interface to the constructor. This allows you to swap in different implementations without having to modify the main class. The Dependency Injection pattern also promotes high cohesion by promoting the Single Responsibility Principle (SRP), since your dependencies are individual objects which perform discrete specialized tasks like data access (via DAOs) and business services (via Service and Delegate classes).
The Inversion of Control (IoC) container is a container that supports Dependency Injection. In this you use a central container like Spring framework, Guice, or HiveMind, which defines what concrete classes should be used for what dependencies throughout your application. This brings in an added flexibility through looser coupling, and it makes it much easier to change what dependencies are used on the fly. The basic concept of the Inversion of Control pattern is that you do not create your objects but describe how they should be created.
You don't directly connect your components and services together in code but describe which services are needed by which components in a configuration file. A container (in the case of the Spring framework, the IOC container) is then responsible for hooking it all up. Applying IoC, objects are given their dependencies at creation time by some external entity that coordinates each object in the system. That is, dependencies are injected into objects. So, IoC means an inversion of responsibility with regard to how an object obtains references to collaborating objects.
For example, in Spring you will wire up the dependencies via an XML file:
<bean id="orderService" class="com.coupling.OrderServiceImpl"> <constructor-arg ref="paymentService"/> </bean> <bean id="paymentService" class="com.coupling.PaymentServiceImpl" />
You can also use annotations to inject dependencies. The @Resource annotation injects PaymentService.
package com.coupling; import java.math.BigDecimal; import javax.annotation.Resource; public class OrderServiceImpl implements OrderService { @Resource PaymentService payService; public OrderServiceImpl(PaymentService payService) { this.payService = payService; } public void process(int accountNumber, BigDecimal amount) { // some logic payService.handlePay(new PaymentDetail(accountNumber, amount)); // some logic } }
Tip #5: High cohesion often correlates with loose coupling, and vice versa.
What is cohesion? Cohesion is the extent to which two or more parts of a system are related and how they work together to create something more valuable than the individual parts. You don't want a single class to perform all the functions (or concerns) like being a domain object, data access object, validator, and a service class with business logic. To create a more cohesive system from the higher and lower level perspectives, you need to break out the various needs into separate classes like PaymentDetail, PaymentService, PaymentDao, PaymentValidator, etc. Each class concentrates on one thing.
Coupling happens in between classes or modules, whereas cohesion happens within a class. So, think, tight encapsulation, loose (low) coupling, and high cohesion.
Tip #6: Favor composition over inheritance for code reuse
You will get a better abstraction with looser coupling with composition as composition is dynamic and takes place at run time compared to implementation inheritance, which is static, and happens at compile-time. The guide is that inheritance should be only used when subclass ‘is a’ super class. Don’t use inheritance just to get code reuse. If there is no ‘is a’ relationship then use composition for code reuse. Overuse of implementation inheritance (uses the “extends” key word) can break all the subclasses, if the super class is modified. Do not use inheritance just to get polymorphism. If there is no ‘is a’ relationship and all you want is polymorphism then use interface inheritance with composition, which gives you code reuse. More elaborate explanation on this -- Why favor composition over inheritance in Java OOP?
You may also like How to create well designed Java classes?
Labels: Java Architect, OO
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home