DEV Community

Fernando Correia
Fernando Correia

Posted on

Cohesion and coupling

Introduction

High cohesion and loose coupling are fundamental engineering principles.

These principles are complementary, and they can (and should) be applied at any level of abstraction in a software system.

Definitions

Cohesion

Cohesion is the degree to which the elements inside a module belong together.

This definition comes straight out of foundational, venerable work that Yourdon and Constantine did back in the 1960s and 1970s. As these are fundamental characteristics, these principles remains fresh and applicable as new technologies and patterns emerge.

A good design is based on highly cohesive modules. A module will mean different things depending on the level of abstraction we're designing at. For instance, at a high level of abstraction, it can be a Docker container, or a microservice. At a lower level of abstraction, it can be a class, or a source code file.

A module is highly cohesive when it contains elements that are closely related to each other, i.e. they "naturally" belong together, and when all such components are in that one module (i.e. not "spread around").

Ideally, a module must have a single, well-defined responsibility, and all of its elements contribute to that responsibility.

In reverse, a less cohesive module (or not cohesive at all) contains a diverse array of loosely-related, or unrelated elements, often at different levels of abstraction.

Coupling

Coupling refers to the degree to which the different modules depend on each other.

Modules that have a high amount of interdependency are said to be "tightly" or "strongly" coupled, and modules that have few interdependencies between them are said to be "loosely" or "weakly" coupled.

A common way to measure the degree of dependency is to find out if a change in one module would have an impact (i.e. require a change) in other modules.

One frequent cause of tight coupling is the implementation of a module having (explicit or implicit) dependencies on the implementation of other modules.

A good design aims for loose coupling between modules. That means few, well-defined, simple dependencies that are based on explicit contracts between the modules.

Depending on the level of abstraction, these "contracts" can be represented as API specifications, message formats, class interfaces, and so on.

Benefits

High cohesion is associated with several benefits. Among them, greater robustness, reliability, reusability and understandability.

Loose coupling produces the benefit that changes in one module don't heavily impact other modules. In particular, changes to the implementation of one module should not affect other modules if the contract hasn't changed. If it does, there likely was an implicit dependency, i.e. one module was making assumptions about the inner working of the other module which were not part of the explicit contract between them.

Examples

Generically speaking, without trying to be too accurate or academic, these are some common examples of these principles in practice:

At a high level of abstraction

A web store can be decomposed into cohesive chunks of functionality, for instance:

  • Catalog
  • Browsing
  • Wish list
  • Shopping cart
  • Orders
  • Billing

And so on.

An aside to avoid confusion: It's important to notice that the logical grouping of these modules has no implication on how they will be delivered as software artifacts. Designing a system in cohesive chunks does not mean that each of these chunks corresponds to exactly one executable artifact, like a service. Each of the modules above could be an independent microservice. Or a set of serverless functions. Or they could be all bundled together into a single monolithic app, that aggregates these logically separated modules into a single runtime deliverable. The important point is that functionality that belongs together is logically organized together, and that dependencies between these cohesive chunks are reduced to a minimum, and are based on explicit contracts.

It's harder to give examples of coupling, since coupling at this high level of abstraction can happen in many different forms, like calls or messages from one module to another, or at the database level, or even via auxiliary services like in-memory caches, or search indexes.

One possible example is that when the user checks out a shopping cart, that generates an event (e.g. a message in a queue, or a database record, or perhaps simply a function call) that will in turn trigger a corresponding action by another module (e.g. create an order), which in turn will trigger another process (e.g. charge credit card). A loosely coupled system will have few, well defined dependencies that assume as little as possible about how that operation will be executed by the other module.

At a lower level of abstraction

A typical low-level module is a class, or a source code file (depending on the language).

One of the most common signs of low cohesion at that level are the so-called "manager" classes. Cohesive classes will have a single responsibility, and their name will reflect that responsibility.

That's the reason why it's commonly said that "naming" is one of the hardest problems in software. It's because in order to properly name something, you need to be clear about what that something means, i.e. for classes, what is the responsibility of that class (and in consequence, which responsibilities will belong to other classes, and not to this one).

For instance, a straw man "BillingManager" class could have among its responsibilities:

  • SendInvoice
  • ChargeCreditCard
  • RevertTransaction
  • DetectFrauds

And so on. That class would not be cohesive, because it's dealing with a large array of responsibilities. While it's true that all these responsibilities are somehow related to "billing", they are too diverse, arguably, to all be handled by a single unit of code.

In a more cohesive design, these responsibilities would be split among several classes, i.e. one dealing with invoicing, another with credit card transactions, and another with fraud detection.

At the class level, tight coupling (and usually, low cohesion, because those often go hand in hand) can be spotted by long dependency lists (like "imports", or injected classes). Another red flag, albeit subtler, is code that depends on, or make assumptions about, the inner workings of other classes.

It's important to acknowledge that loose coupling, particularly at the class level, is not a goal in itself. The goal is to be able to make changes in the code with low impact, and low risk. The degree of coupling between classes is on a spectrum. It may be preferable to have a few dependencies between a group of closely related classes rather than introducing an artificial abstraction which often muddles the issue and makes the structure of the code harder to understand, while just shifting the coupling to another place.

One particular point to note is that de-duplication, i.e. DRY (don't repeat yourself) almost always introduces coupling (i.e. a dependency to an extracted function or class). That doesn't mean that's bad, but it's important to keep in mind that it has a cost, and that now changing that extracted piece of functionality will have an impact everywhere it is used. That can be a problem if the abstraction was leaky (which can often be spotted by classes or functions with long parameter lists and multiple "options" or "modes").

Performance is also a concern when deciding what the correct level of coupling is, and it's also a factor when choosing between fine-grained or coarse-grained interfaces. In system design, everything is a trade-off. It is important to be cognizant of the pros and cons.

Takeaways

In general terms:

  • High cohesion & loose coupling good.
  • Low cohesion & tight coupling bad.

To learn more

Top comments (0)