As I was finishing my blog post about defining service boundaries, I had a very strong feeling that there must be some abstract concept of what I was trying to explain using actual examples…
Of course, there is! It is the concept of cohesion and coupling that I will discuss in this post.
I will start with some definitions:
Cohesion is the degree to which the elements inside a module belong together.
Coupling is the degree of interdependence between software modules.
High cohesion and loose coupling are the most important principles in software engineering. They manifest themselves everywhere from code to team organization.
Cohesion and coupling are tightly related. Why are they so important? Both help us reduce complexity, the true fun killer of software development.
To a lot of people, sadly, the concepts sound too academic and are therefore often poorly understood.
What is cohesion, anyway?
Tough question. The definition is pretty broad and there are several interpretations out there. Not all of them are necessarily wrong, the more valid question is: which one is most beneficial? I use the following definition as I believe it always leads to cohesive components with tight coupling inside and loose coupling outside, which is exactly what we want:
The degree of cohesion of a component by a particular key equals the number of elements cohesive by the key within the component divided by the sum of the total number of elements cohesive by the key in the whole system and the number of elements not cohesive by the key inside the component.
This long definition can be expressed as a simple formula:
Where c
stands for the component, k
stands for the key, and N
stands for the number of elements. Obviously, the maximal cohesion of a component is equal to one. This is what we strive for.
I want to emphasize that cohesion does not depend on the number of connections between elements, which is what coupling is all about. Cohesion is more about belonging together. However, cohesive components do tend to have a higher degree of coupling within the component, but that is just a symptom of high cohesion, not the cause.
The definition above might look complicated, but it is actually quite easy. It can be illustrated by some examples. We measure the degree of cohesion by the violet key for the components bordered with a dashed line in the following systems:
Functionality (business) is always the right key to use. Violet and blue can stand for sales and accounting, a product and an invoice, or user registration and ordering.
My definition may be a bit oversimplified as the boundaries are not always as solid and obvious. This is why business experts must be involved.
Myth busted
Cohesion and coupling are almost always discussed together as they tightly correlate. The relation is sometimes a source of confusion as well, although its understanding is very useful to gain the utmost for the software system under development.
A typical myth, I often hear people believe in, puts cohesion and coupling in opposition. Practically, they say that “the higher the cohesion the tighter the coupling”. I will show you how wrong this statement is.
This is usually illustrated with an example: Consider the highest possible cohesion of the system where every module is represented by a single line of code (or a single function, an object with a single method, etc.). Such a degree of cohesion will inevitably increase the coupling between modules to the maximum.
As the conclusion is true, there is a small problem in the prerequisite. To discover it, we must recall the definition of cohesion once again. It talks about belonging together, the strength of relationship of elements, and a common purpose.
What does it mean in practice? In fact, splitting elements that belong together actually makes cohesion lower. So, in the example above, the system really does not have the highest possible cohesion, just the opposite: breaking modules into the smallest possible elements will separate related concepts and will lead to pretty low cohesion.
The moral here is: Cohesion is not something you can create automatically. Cohesion is discovered in a particular context. That is why it is so hard for cohesion to be reliably measured. We will discuss this in detail later, stay tuned.
Cohesion and coupling
Let me show you some pictures. In each figure below, there are the very same elements with the very same dependencies. Those are further differently organized. Related domain concepts are represented with the same color:
Elements in the first picture have no explicit boundaries; they are an example of so-called coincidental cohesion. Such architecture is known as the Big Ball of Mud or the God Object (in OOP code).
The second picture shows a system with three modules and a lot of dependencies between them. Although the modules are highly cohesive, they are cohesive by the wrong key. This happens when code is organized by other than a domain relationship. A typical example is a logical organization of code in the Layered Architecture: just imagine modules such as controllers, repositories, services, etc. Have you seen these already somewhere? Hell yeah!
The system in the third picture shows the ideal scenario: correctly organized modules leading to high cohesion and loose coupling. The right key for organization is functionality, in other words, a business domain. The domain defines abstractions with a stable purpose the cohesion is driven upon. By the way, that is the main idea of the Domain-Driven Design.
Focus on cohesion, not coupling
We have exhausted all variants except one: a system with low cohesion and loose coupling. Is it even possible to have such architecture? Unfortunately, it is, and it is actually pretty common.
Systems with low cohesion and loose coupling are the result of incorrect understanding of the domain and applying purely technical approaches to decouple the modules in an arbitrary way. Interfaces everywhere with no abstraction representing a domain purpose are typical for systems built in this way.
Misuse of interfaces will not actually reduce coupling anyway, it just moves it into the runtime.
Striving for loose coupling at any cost can (and will) harm cohesion. As loose coupling is driven by high cohesion, we should strive for high cohesion in the first place.
Level of abstraction
Yes, high cohesion does not only make the system easy to understand and change, it also reduces the level of coupling.
How is this even possible? Common sense says that the dependencies do not disappear simply by reorganizing elements. While this is true for the overall system dependencies, high cohesion does reduce dependencies on a higher level of abstraction.
That is, although the absolute amount of dependencies remains the same, the coupling is tackled on different levels of abstraction.
The whole is greater than the sum of the parts. ~ Aristotle
Indeed, we can ignore the interdependencies inside modules and thus get a simplified big picture with only three loosely coupled elements:
Neat. As we see, high cohesion actually results in loose coupling!
Talk to me in code!
Pictures are nice, but as software developers, we trust only code, don’t we? Alright, I have some code for you. Consider a simple class for a Book Store (in JavaScript, whatever):
class BookStore {
add(book) { … }
remove(book) { … }
sale(book) { … }
receiptFor(book) { … }
}
This class does literally everything. Its cohesion is pretty low and all clients, whatever their needs are, will be coupled to it. It is an example of a God Object. We can do better:
class Inventory {
add(book) { … }
remove(book) { … }
}
class Sales {
sale(book) { … }
receiptFor(book) { … }
}
The Inventory
class looks fine, but what about Sales
? Must sales and accounting really be so tightly related? Maybe it would be better to split the functionalities into more cohesive classes:
class Sales {
sale(book) { … }
}
class Accounting {
receiptFor(book) { … }
}
But what if our Book Store is just a small family business with one clerk handling sales together with accounting on one old cash desk? We have just hit the nail on the head: we cannot really know what the right cohesion key is unless we know the domain really well. True cohesion is defined by the clients. High cohesion is achieved when there is no way to split the module any further while still satisfying the needs of the client. By the way, this is exactly what the Single Responsibility Principle teaches us.
Conclusion
High cohesion and loose coupling are the main design drivers towards simple system architecture, which is easy to understand, change, and maintain. High cohesion and loose coupling help us reduce accidental complexity and create modules with well-defined boundaries.
- Coupling is about connections, cohesion is about belonging together.
- Cohesion cannot be created automatically; instead it is discovered in a context.
- Cohesion is defined by the clients.
- True cohesion is domain-driven.
- High cohesion results in loose coupling.
- High cohesion is to die for. It enables all others, loose coupling included.
Originally published on my blog.
Top comments (1)
Thanks for explaining. It's worth reading 😃