The Ying And Yang Of Software Development
Let’s discuss coupling and cohesion, and how to use the Single Responsibility Principle to get the right balance.
Coupling
Coupling represents the degree of interdependence between software components. In simpler terms, if a change in a component directly affects another component, they are coupled.
For instance, in the code snippet below, the EmalService class directly depends on the CustomerService:
@RequiredArgsConstructor
public class EmailService {
private static final String orderPlacedTemplate = """
Dear {},
Your order was successfully placed.
It will be delivered your address ({}), in 3 working days.
Thank you,
Team Dhl.
""";
private final CustomerService customerService;
public void sendOrderPlacedEmail(String username) {
Customer customer = customerService.findByUsername(username);
String emailBody = MessageFormat.format(orderPlacedTemplate,
customer.getFullname(),
customer.getAddress().getAddressLineOne());
sendEmail(customer.getEmail(), emailBody);
}
private void sendEmail(Email email, String body) {
// ....
}
}Consequently, if the CustomerService changes, the EmailService might be affected: they are coupled together. Though,
We can break this coupling by making sendOrderPlacedEmail receive the Customer object as a parameter:
public void sendOrderPlacedEmail(Customer customer) {
String emailBody = MessageFormat.format(orderPlacedTemplate,
customer.getFullname(),
customer.getAddress().getAddressLineOne());
sendEmail(customer.getEmail(), emailBody);
}
private void sendEmail(Email email, String body) {
// ....
}This way, the EmailService will not have the responsibility to retrieve the customer and will not know anything about the CustomerService.
Cohesion
Software cohesion is a measure of how well the individual parts of a software system work together as a single unit.
Let’s consider the following methods from different classes:
public String generateOrderInTransitMessage(
String firstName, String lastName, String honoraryName) {
//...
}
public void sendOrderDeliveredEmail(
String firstName, String lastName, String honoraryName, Email email) {
//...
}
public void sendOrderDeliveredNotification(
String firstName, String lastName, String honoraryName, Channel channel) {
//...
}We can quickly notice that the firstName, lastName, and honoraryName fields are always being passed together. This can be a good indicator that they are highly cohesive.
One of our goals when designing an application is to make sure that all the components have high cohesion and loose coupling. Therefore, it can be a good idea to group the three fields together in a small class:
public record FullName (
String firstName,
String lastName,
String honoraryTitle) {
}
// and
public String generateOrderInTransitMessage(FullName fullNam) {
//...
}
public void sendOrderDeliveredEmail(FullName fullName, Email email) {
//...
}
public void sendOrderDeliveredNotification(FullName fullName, Channel channel) {
//...
}Moreover, fields with high cohesion that are not extracted into a separate class tend to lead to code duplication. For example, we can imagine similar validations for the name fields in the 3 given methods or using a similar way of formating the name. We can avoid this by enriching the FullName class with validation and small methods:
public record FullName (String firstName, String lastName, String honoraryTitle) {
public FullName(String firstName, String lastName, String honoraryTitle) {
Assert.notNull(firstName, "'first name' cannot be null.");
Assert.notNull(lastName, "'last name' cannot be null.");
this.firstName = firstName;
this.lastName = lastName;
this.honoraryTitle = honoraryTitle;
}
public String getFullName() {
StringBuilder sb = new StringBuilder();
if(StringUtils.isNotBlank(honoraryTitle)) {
sb = sb.append(honoraryTitle).append(" ");
}
return sb.append(firstName)
.append(" ")
.append(lastName)
.toString();
}
}Common Mistake
A common mistake is to group functions together just because they revolve around the same domain object (usually, this will be a database entity).
For instance, in the example below we have 4 methods related to the Order entity. How much do they cohere? Do they belong to the same class?
public class OrderService {
public void placeOrder(Customer customer, Order order) {
// ...
}
public void returnOrder(CancellationReason reason, Order order) {
// ...
}
public List<Order> findOrdersByCustomerId(long customerId) {
// ...
}
public List<Order> findAllCanceledOrdersBetween(Date startDate, Date endDate) {
// ...
}
}Let’s take them one by one, and imagine their purposes based on the given names:
- placeOrder will probably save a new order in the database and link it to a customer. This will be called when a user places an order on the website.
- returnOrder is going to mark the order as canceled and return the package to the center. This function will be called when the delivery team encounters difficulties delivering the package.
- findOrdersByCustomerId is most probably needed by the front end to display a list of all the orders when a customer logs in.
- findAllCanceledOrdersBetween is needed by a different module, which is checking if all the canceled orders have arrived back at the center.
As we can see, these methods do not have a big cohesion, but, at the same time, we wouldn’t want to have one class per method either. Though, if we follow the Single Responsibility Principle, we can split our system into components with high cohesion and low coupling.
Single Responsibility Principle
We all know the Single Responsibility Principle, the “S” in SOLID: “A class should only have one responsibility” or “A class should only have one reason for change”.
Looking back to our previous example, how many reasons are there for changing it?
- we can change it if the UI changes when a customer logs into the website
- we can change it if the application that the delivery team is using introduces a new use-case
- we can change it if the “housekeeper” module that is checking for canceled orders changes
As Uncle Bob explains in this article, a “responsibility” can be defined as a group of users or clients of our application. In our case, we have 3 potential responsibilities.
// this will be used by the customers form the website:
public class OrderDisplayService {
public void placeOrder(Customer customer, Order order) {
// ...
}
public List<Order> findOrdersByCustomerId(long customerId) {
// ...
}
}
// this will be used by the delivery team from their client app
public class OrderDeliveryService {
public void returnOrder(CancellationReason reason, Order order) {
// ...
}
}
// this will be used by the operators and the 'housekeeping' app
public class OrderHousekeepingService {
public List<Order> findAllCanceledOrdersBetween(Date startDate, Date endDate) {
// ...
}
}Conclusion
In this article, we discussed coupling, cohesion, and the Single Responsibility Principle. We learned that cohesion is the “force” that keeps similar functionalities together, in a single component, and that “coupling” is the interdependence between these components.
After that, we saw that a common mistake is to organize our components solely based on the entities they revolve around. Then, we fixed this by applying the Single Responsibility Principle, and we saw how this principle can lead to components with loose coupling and high cohesion.
Thank You!
Thanks for reading the article and please let me know what you think! Any feedback is welcome.
If you want to read more about clean code, design, unit testing, functional programming, and many others, make sure to check out my other articles. Do you like the content? Consider following or subscribing to the email list.
Finally, if you consider becoming a Medium member and supporting my blog, here’s my referral.
Happy Coding!
