Decoding Coupling and Cohesion

Coupling and Cohesion are two important concepts in software engineering that describe the quality of a software design.

Definition

Coupling

Coupling refers to the degree to which one component in a software system is dependent on another component. When components are tightly coupled, changes to one component can have a significant impact on other components.

Cohesion

Cohesion, on the other hand, refers to the degree to which the elements within a single component are related to each other. High cohesion means that the elements within a component work together to achieve a single, well-defined purpose.

Highs or Lows?

We desire Low Coupling & High Cohesion.

Tight coupling can lead to fragility, where small changes can have unexpected and far-reaching consequences. Loosely coupled components, on the other hand, are more flexible and can be easily replaced or modified without affecting other components.

Low cohesion means that the elements within a component are not closely related and may not have a clear purpose. High cohesion is generally desirable because it makes the code easier to understand, modify, and maintain.

coupling and cohesion

Coupling

High coupling refers to a situation where software components or modules have strong interdependencies, which can lead to complex and brittle code that is difficult to modify or maintain. Here is an example of high coupling in code:

public class CustomerService {
    private Database db;
    private Logger logger;
    
    public CustomerService() {
        this.db = new Database();
        this.logger = new Logger();
    }
    
    public void addCustomer(Customer customer) {
        try {
            db.connect();
            db.insert(customer);
            logger.log("Customer added: " + customer.getName());
            db.disconnect();
        } catch (Exception ex) {
            logger.logError(ex.getMessage());
        }
    }
    
    public void updateCustomer(Customer customer) {
        try {
            db.connect();
            db.update(customer);
            logger.log("Customer updated: " + customer.getName());
            db.disconnect();
        } catch (Exception ex) {
            logger.logError(ex.getMessage());
        }
    }
    
    // Other methods...
}
Java

n this code, the CustomerService class has a strong dependency on the Database and Logger classes, which are tightly coupled. The CustomerService class creates instances of the Database and Logger classes directly in its constructor, and it uses them throughout its methods. This makes it difficult to reuse or replace these dependencies, and it makes the CustomerService class highly dependent on the behavior of these classes.

As a result, if the Database or Logger classes change, it may require changes to the CustomerService class, which can result in complex and error-prone code. Additionally, it can be difficult to test the CustomerService class in isolation, as it depends on the behavior of the Database and Logger classes.

To address this issue of high coupling, software engineers can use techniques such as dependency injection or inversion of control, to decouple the CustomerService class from its dependencies, and make it easier to modify, maintain, and test.

public class CustomerService {
    private Database db;
    private Logger logger;
    
    public CustomerService(Database db, Logger logger) {
        this.db = db;
        this.logger = logger;
    }
    
    public void addCustomer(Customer customer) {
        try {
            db.connect();
            db.insert(customer);
            logger.log("Customer added: " + customer.getName());
            db.disconnect();
        } catch (Exception ex) {
            logger.logError(ex.getMessage());
        }
    }
    
    public void updateCustomer(Customer customer) {
        try {
            db.connect();
            db.update(customer);
            logger.log("Customer updated: " + customer.getName());
            db.disconnect();
        } catch (Exception ex) {
            logger.logError(ex.getMessage());
        }
    }
    
    // Other methods...
}
Java

In this refactored code, the Database and Logger instances are passed in as constructor arguments to the CustomerService class, rather than being created directly in the class. This makes the CustomerService class less dependent on the Database and Logger classes, and more flexible and reusable.

Additionally, this refactored code is easier to test, as we can now pass in mock instances of the Database and Logger classes to the CustomerService constructor for testing purposes. Overall, this refactored code has lower coupling, which makes it more maintainable, flexible, and testable.

Types of Coupling

  1. Content Coupling: This is the strongest form of coupling, where one module directly modifies or accesses the internal data of another module.
  2. Common Coupling: This occurs when two or more modules share a global data object. Changes to the global data can have an impact on all modules that use it.
  3. Control Coupling: This occurs when one module passes control information (e.g., flags, signals) to another module, which then uses that information to make decisions.
  4. Stamp Coupling: This occurs when two or more modules share a composite data structure, but each module only uses a portion of the data structure.
  5. Data Coupling: This occurs when two or more modules communicate by passing data between them. Data coupling can be further classified as loose coupling, where data is passed in a generic format, or tight coupling, where data is passed in a specific format.
  6. Message Coupling: This occurs when two or more modules communicate by passing messages between them. The sender module does not need to know anything about the receiver module, except for the message format.

Benefits of Low Coupling

  1. Flexibility: Low coupling enables components to be easily swapped or replaced without affecting the rest of the system. This allows for greater flexibility and adaptability to changing requirements.
  2. Maintainability: With low coupling, changes to one component do not have a ripple effect on the rest of the system. This makes it easier to maintain and modify the software system over time.
  3. Testability: Low coupling makes it easier to test individual components in isolation, as they have minimal dependencies on other components. This allows for more effective testing and easier debugging.
  4. Scalability: As the system grows and evolves, low coupling allows for easier scaling of individual components without affecting the rest of the system. This enables greater scalability and can help avoid performance bottlenecks.
  5. Reusability: Low coupling enables individual components to be reused in other parts of the system or in other software projects. This can lead to greater efficiency and cost savings in software development.

Reasons for High Coupling

  1. Poor Design: A poor software design can lead to high coupling between components. For example, if components are not properly abstracted, they may be tightly coupled to one another.
  2. Lack of Modularity: When a software system is not modular, it can be difficult to achieve low coupling between components. This can occur if there is no clear separation of concerns or if modules are not well-defined.
  3. Tight Integration: In some cases, it may be necessary for components to be tightly integrated, leading to high coupling. For example, in real-time systems, components may need to be tightly coupled to ensure that they can communicate and respond quickly.
  4. Legacy Code: High coupling may also occur in legacy code that has been poorly maintained over time. As software systems evolve and change, it can be difficult to maintain a low-coupling design without significant refactoring.
  5. Third-Party Libraries: When using third-party libraries, high coupling may occur if the library is tightly integrated with the software system. This can make it difficult to modify or replace the library without impacting other components.

How to achieve Low Coupling

  1. Abstraction: By abstracting components and hiding their implementation details, software engineers can reduce the dependencies between components, which can lead to low coupling.
  2. Encapsulation: Encapsulation involves grouping related data and operations within a single component and exposing only the necessary interfaces to other components. This can help reduce dependencies between components and achieve low coupling.
  3. Modularity: Modularity involves dividing a software system into independent, reusable components, each with a well-defined responsibility. By defining clear interfaces between modules and minimizing dependencies, software engineers can achieve low coupling.
  4. Dependency Injection: By using dependency injection frameworks, components can be loosely coupled and dependencies can be managed at runtime. This can help reduce the amount of coupling between components.
  5. Event-Driven Architecture: In an event-driven architecture, components communicate with one another through events rather than direct method calls. This can help reduce dependencies between components and achieve low coupling.

Cohesion

Here’s an example of a Java class with bad cohesion:

public class Employee {
    private String name;
    private int age;
    private String address;
    private String email;
    private int salary;
    
    public void calculateSalary() {
        // code to calculate salary
    }
    
    public void sendEmail() {
        // code to send email
    }
    
    public void printEmployeeDetails() {
        // code to print employee details
    }
    
    // other methods...
}
Java

In this example, the Employee class has low cohesion, as it has several unrelated responsibilities. The class has properties for storing employee details such as name, age, address, and email, as well as a method for calculating the employee’s salary, sending an email, and printing employee details. These responsibilities are not directly related to each other, which makes the class difficult to understand, use, and modify.

To refactor the Employee class and improve its cohesion, we can split it into smaller, more focused classes, each with a single, well-defined responsibility. For example:

public class Employee {
    private String name;
    private int age;
    private Address address;
    private ContactDetails contactDetails;
    private Salary salary;
    
    // constructor and getters/setters omitted for brevity
}
Java

Each of these classes has a single, well-defined responsibility and is focused on a specific aspect of an employee’s information.

Types of Cohesion

  1. Functional Cohesion: This occurs when the elements within a component are related to a single, well-defined function or task. The code is organized around a specific operation or procedure.
  2. Sequential Cohesion: This occurs when the elements within a component are related to a sequence of tasks that must be executed in a specific order. The code is organized around a specific sequence of operations.
  3. Communicational Cohesion: This occurs when the elements within a component are related to a specific data structure or communication protocol. The code is organized around a specific data structure or message format.
  4. Procedural Cohesion: This occurs when the elements within a component are related to a general category or type of operation. The code is organized around a specific type of procedure.
  5. Temporal Cohesion: This occurs when the elements within a component are related to a specific time period or event. The code is organized around a specific event or time period.
  6. Logical Cohesion: This occurs when the elements within a component are related by logic or control flow. The code is organized around a specific set of logical rules or conditions.

Benefits of High Cohesion

  1. Clarity: High cohesion makes it easier to understand the purpose and functionality of a component. Each component has a clear responsibility or reason for existence, and its methods are focused on a specific set of related tasks.
  2. Maintainability: With high cohesion, changes to a component are typically limited to a specific set of related tasks. This makes it easier to maintain and modify the component over time, as there is less complexity and fewer dependencies to consider.
  3. Reusability: High cohesion makes it easier to reuse components in other parts of the system or in other software projects. A cohesive component can be more easily integrated into different contexts, as it has a clear and well-defined purpose.
  4. Testability: High cohesion makes it easier to test a component, as its methods are focused on a specific set of related tasks. Testing can be more targeted and effective, leading to fewer bugs and higher software quality.
  5. Performance: High cohesion can lead to better performance, as each component is focused on a specific set of related tasks and can be optimized accordingly. This can result in faster and more efficient software.

Reasons for Low Cohesion

  1. Poor Design: A poor software design can lead to low cohesion between components. For example, if components are not properly abstracted, they may be loosely related to one another.
  2. Unnecessary Features: If a component has too many features or responsibilities that are not closely related, it can result in low cohesion. This can occur if developers try to cram too much functionality into a single component.
  3. Lack of Modularity: When a software system is not modular, it can be difficult to achieve high cohesion between components. This can occur if there is no clear separation of concerns or if modules are not well-defined.
  4. Inconsistent Naming Conventions: If the naming conventions used within a software system are inconsistent, it can make it difficult to identify the relationships between components. This can result in low cohesion.
  5. Inadequate Testing: If a software system has inadequate testing, it can be difficult to identify and resolve low cohesion issues. This can result in code that is difficult to modify, maintain, and extend.

How to achieve High Cohesion

  1. Single Responsibility Principle: Each component should have a single responsibility or reason to change. This can help ensure that the component has high cohesion and is focused on a specific set of related tasks.
  2. Abstraction: By abstracting components and defining clear interfaces, software engineers can help ensure that each component is responsible for a specific set of related tasks, leading to high cohesion.
  3. Modularity: Modularity involves dividing a software system into independent, reusable components, each with a well-defined responsibility. By defining clear responsibilities for each module and minimizing overlap, software engineers can achieve high cohesion.
  4. Naming Conventions: By using clear and consistent naming conventions for components and their methods, software engineers can help ensure that each component is focused on a specific set of related tasks.
  5. Testing: Regular testing can help ensure that components are focused on a specific set of related tasks and have high cohesion. By testing components in isolation and ensuring that they meet their responsibilities, software engineers can ensure that each component is cohesive.

Wiki: https://en.wikipedia.org/wiki/Coupling_(computer_programming)

WIKI: https://en.wikipedia.org/wiki/Cohesion_(computer_science)