Loading...
Skip to Content

Cohesion and Coupling in Software Design

 Cohesion and coupling are two of the most important concepts in software design. Today we will use everyday objects to make these concepts easy to understand, and then apply them to object-oriented programming and microservices architecture.

Cohesion and coupling in software design (a real-life example: thread, scissors, nippers, wire)

A real-life example: thread, scissors, nippers, wire

Think of the thread, scissors, nippers, and wire around us. What is the most efficient way to group them?

  1. Group by material: thread and wire / scissors and nippers
  2. Group by purpose: thread and scissors / wire and nippers

The first approach may look logical at first glance. But in terms of actual use, the second approach is far more efficient. Why is that?

 The benefits of high cohesion

Grouping thread with scissors and wire with nippers gives you the following benefits.

  • Data locality: The tools and materials you need are close together, making work more efficient.
  • Ease of maintenance: Related tools and materials are kept together, so they are easy to manage.
  • Independence: Each group can operate independently, so it can be modified without affecting the other group.

Let's apply these concepts to software design.

 Cohesion in object-oriented programming

Here is example code in Python.

Example of low cohesion

class LowCohesion: def __init__(self): self.thread = "thread" self.wire = "wire" def cut_thread(self): print(f"Cutting the {self.thread}.") def cut_wire(self): print(f"Cutting the {self.wire}.")

Example of high cohesion

class SewingKit: def __init__(self): self.thread = "thread" self.scissors = "scissors" def cut(self): print(f"Cutting the {self.thread} with the {self.scissors}.") class ElectricalKit: def __init__(self): self.wire = "wire" self.nipper = "nippers" def cut(self): print(f"Cutting the {self.wire} with the {self.nipper}.")

The LowCohesion class has low cohesion because it holds the unrelated thread and wire together. The SewingKit and ElectricalKit classes, by contrast, have high cohesion because they bundle related data and behavior together.

 Cohesion in microservices

The same concept can be applied to microservices architecture.

Sewing microservice

class SewingService: def __init__(self): self.thread = "thread" self.scissors = "scissors" def cut_thread(self): return f"Cutting the {self.thread} with the {self.scissors}." def measure_thread(self): return f"Measuring the length of the {self.thread}."

Electrical work microservice

class ElectricalService: def __init__(self): self.wire = "wire" self.nipper = "nippers" def cut_wire(self): return f"Cutting the {self.wire} with the {self.nipper}." def strip_wire(self): return f"Stripping the insulation from the {self.wire}."

Each microservice focuses on a specific domain (sewing or electrical work), so it has high cohesion. Designed this way, each service can be developed, deployed, and scaled independently.

 Problems when coupling is high

A design with low cohesion and high coupling can cause the following serious problems. Returning to our real-life example, suppose we grouped the scissors and nippers together in one group, and the thread and wire together in another.

  1. Frequent API calls: Dependencies between different modules or services grow, driving up unnecessary API calls. This increases network load and degrades the performance of the entire system.

    Example:

    class HighlyCoupledToolService: def __init__(self): self.scissors = "scissors" self.nipper = "nipper" def cut_thread(self): return f"cut with {self.scissors}" def cut_wire(self): return f"cut with {self.nipper}" class HighlyCoupledMaterialService: def __init__(self): self.thread = "thread" self.wire = "wire" def get_thread(self): return self.thread def get_wire(self): return self.wire class SewingTask: def __init__(self): self.tool_service = HighlyCoupledToolService() self.material_service = HighlyCoupledMaterialService() def perform_sewing(self): thread = self.material_service.get_thread() # unnecessary API call cut_action = self.tool_service.cut_thread() return f"{thread}: {cut_action}" class ElectricalTask: def __init__(self): self.tool_service = HighlyCoupledToolService() self.material_service = HighlyCoupledMaterialService() def perform_wiring(self): wire = self.material_service.get_wire() # unnecessary API call cut_action = self.tool_service.cut_wire() return f"{wire}: {cut_action}"

    In this example, SewingTask and ElectricalTask must call both HighlyCoupledToolService and HighlyCoupledMaterialService on every operation. In a real microservices environment, this translates into unnecessary network calls.

  2. Difficult maintenance: A change in one part ripples out to many others, making modifications complex and risky. For example, changing how the scissors are used may require editing both HighlyCoupledToolService and SewingTask.
  3. Increased testing complexity: High coupling makes unit testing hard. To test SewingTask, you need mock objects for both HighlyCoupledToolService and HighlyCoupledMaterialService.
  4. Reduced system extensibility: Adding a new tool or material means modifying several services at once. For example, adding a new material such as a 'rubber band' requires changing HighlyCoupledMaterialService and every task class that uses it.
  5. Lower code reusability: It becomes hard to reuse HighlyCoupledToolService or HighlyCoupledMaterialService in other projects, because each service depends heavily on the other.

 Improved Design: High Cohesion, Low Coupling

To solve the problems above, let's redesign the code to raise cohesion and lower coupling.

class SewingKit: def __init__(self): self.thread = "thread" self.scissors = "scissors" def cut_thread(self): return f"cut {self.thread} with {self.scissors}" class ElectricalKit: def __init__(self): self.wire = "wire" self.nipper = "nipper" def cut_wire(self): return f"cut {self.wire} with {self.nipper}" class SewingTask: def __init__(self): self.sewing_kit = SewingKit() def perform_sewing(self): return self.sewing_kit.cut_thread() class ElectricalTask: def __init__(self): self.electrical_kit = ElectricalKit() def perform_wiring(self): return self.electrical_kit.cut_wire()

In this improved design:

  1. Each kit (SewingKit, ElectricalKit) holds its related tools and materials together, so cohesion is high.
  2. Each task (SewingTask, ElectricalTask) uses only the kit it needs, so coupling is low.
  3. Unnecessary API calls have been eliminated.
  4. Each class has a clear responsibility, making maintenance and testing easier.
  5. Adding new kits or tasks is now straightforward.

 Conclusion

Pursuing high cohesion and low coupling in software design is like keeping thread with scissors and wire with nippers. It improves code readability, simplifies maintenance, and raises the overall quality of the system.

Conversely, low cohesion and high coupling increase system complexity and make maintenance and extension difficult. It is like tossing thread, scissors, nippers, and wire into one random pile, so that you have to rummage through everything each time you need a tool.

Whether in object-oriented programming or microservices architecture, applying this principle well leads to more efficient, more manageable systems. Designing for high cohesion and low coupling lets you build software that is more stable and easier to maintain over the long term.