Code That Stands the Test of Time: Why Cohesion and Coupling Matter
The power for high cohesion and low coupling
Have you ever heard of Clean architecture, Onion architecture, or Hexagonal architecture? Do you know what they have in common?
All these architectures preach Low Coupling and High Cohesion.
Low Coupling
Low Coupling means that different parts of the system are not overly dependent on each other. The Dependency Inversion Principle (DIP) can help achieve this.
Good Example:
Imagine you’re building an e-commerce application. You have a module for Payment Processing and another for Email Notifications. When a customer places an payment, the system sends them a confirmation email.
Instead of the Payment Processing module directly calling the Email Notifications module (high coupling), you can introduce an Event System. The Payment Processing module only publishes an event, like "PaymentAccepted." The Email Notifications module listens for this event and sends the email.
Bad example:
Another example that highlights the necessity of the DIP, is a payment system in an application. Suppose you want the system to support multiple payment methods, such as credit cards, PayPal, and bank transfers.
If the payment handling class directly depends on specific implementations for each provider (e.g., CreditCardProcessor
, PayPalProcessor
), it creates high coupling. This tight dependency makes it harder to add new payment methods or modify existing ones without changing the core payment handling class, which goes against low coupling.
To reduce this coupling, we can introduce an abstraction, like a PaymentProcessor interface, and apply the DIP. In this case, the payment handling class will depend on this interface rather than the concrete implementations. This way, the application becomes more modular, as new payment methods can be added by implementing the interface without changing the existing code.
High Cohesion
High Cohesion ensures that each module focuses on a Single Responsibility Principle (SRP), improving clarity and reducing the likelihood of errors. Together, these principles create systems that are easier to understand, modify, and extend.
Good example:
In the same payment system, let’s design a PaymentService class to handle all payment-related tasks. In this case, the PaymentService class demonstrates high cohesion and adheres to the SRP when its methods focus solely on tasks related to payment management.
Bad example:
Now, let’s consider a poorly designed PaymentService class that violates both High Cohesion and the Single Responsibility Principle (SRP). In this example, the class PaymentService, is responsible for handling the payment, notifying the user, and also validating the user information. The class PaymentService tries to handle multiple unrelated responsibilities, leading to a messy and difficult-to-maintain design.
Conclusion
Before choosing the best architecture for our project, we should understand these principles and apply them correctly to achieve the best results. By doing so, developers can build applications that are resilient to change and designed for long-term success. The right architecture not only makes the system scalable but also ensures cleaner code and smoother collaboration within the team.