Low coupling refers to the degree to which components depend on each other by their lower structures instead of their interfaces, creating a tight coupling among them. Let's make this easier to understand by using a simple example. Imagine that you need to work on the next user's story:
As a bank customer, I want to receive my bank statement by email or fax in order to avoid having to open the bank application.
As you may discover, the developer should work on two things to solve this problem:
- Adding the ability to save the user's preferences in the system
- Making it possible to send the bank statement to the customer by using the requested notification channels
The first requirement seems quite straightforward. To test this implementation, we would use something fairly simple, such as the following code:
@Test
public void
theNotificationChannelsAreSavedByTheDataRepository()
throws Exception
{
// Test here
}
For the second requirement, we will need to read these preferred notification channels and send the bank statement using them. The test that will guide this implementation will look like the following:
@Test
public void
theBankStatementIsSendUsingThePreferredNotificationChannels()
throws Exception
{
// Test here
}
It is now time to show a tightly coupled code in order to understand this problem. Let's take a look at the following implementation:
public void sendBankStatement(Customer customer)
{
List<NotificationChannel> preferredChannels = customerRepository
.getPreferredNotificationChannels(customer);
BankStatement bankStatement = bankStatementRepository
.getCustomerBankStatement(customer);
preferredChannels.forEach
(
channel ->
{
if ("email".equals(channel.getChannelName()))
{
notificationService.sendByEmail(bankStatement);
}
else if ("fax".equals(channel.getChannelName()))
{
notificationService.sendByFax(bankStatement);
}
}
);
}
Note how this code is tightly coupled with the implementation of the NotificationService class; it even knows the name of the methods that this service has. Now, imagine that we need to add a new notification channel. To make this code work, we will need to add another if statement and invoke the correspondent method from this class. Even when the example is referring to tightly coupled classes, this design problem often occurs between modules.
We will now refactor this code and show its low-coupled version:
public void sendBankStatement(Customer customer)
{
List<NotificationType> preferredChannels = customerRepository
.getPreferredNotificationChannels(customer);
BankStatement bankStatement = bankStatementRepository
.getCustomerBankStatement(customer);
preferredChannels.forEach
(
channel ->
notificationChannelFactory
.getNotificationChannel(channel)
.send(bankStatement)
);
}
This time, the responsibility to get a notification channel is passed to the Factory class, no matter what kind of channel is needed. The unique detail that we need to know from the channel class is that it has a send method.
The following diagram shows how the class that sends notifications was refactored to send notifications using different channels and support an interface in front of the implementations per notification channel:
Classes after refactoring
This small but significant change has to lead us to encapsulate the details of the mechanism used to send notifications. This exposes only one well-defined interface that should be used by the other classes.
Although we have shown this example using classes, the same principle is applicable to components, and the same strategies should be used to implement them and avoid coupling among them.