Cohesion means you make sure that the parts that belong together are close to each other. 💥 Bam! Shortest blog post ever!
On a more serious note let's talk about why this is important. If you would have asked me ten years ago I probably would have known about principles like DRY but not about cohesion and its evil twin coupling. I asked my team. Everyone knew and could explain the DRY principle. Some had heard about cohesion but no one could come up with a good explanation. Why is the one principle easier to learn and understand than the other? I think principles like DRY are easier to apply because they come with a set of distinct rules. If you find the same code block multiple times then you're code probably isn't DRY. In contrast, cohesion does not have such a set of rules you can follow. You rather need something we call "Fingerspitzengefühl" in Germany.
TL;DR
This is a tough article and I recommend to read it top-to-bottom. But as always here's the list of key takeaways. To achieve high cohesion you need to:
- understand the difference between re-use and re-purpose,
- be aware of the two kinds of coupling that exist, and
- know about the influence this has on your application design
Re-use what can be re-used but not more
I've mentioned the DRY principle before. That's because I believe that especially less experienced developers can be tricked into bad application design decisions while trying to not duplicate code.
Let's assume you're building an application where users have a profile. That's why you implemented a UserProfile
component.
function UserProfile({ user }) {
return <div>{user.fullName}</div>
}
One day you get a ticket that your customer support department needs a new feature to block certain users. To give support some context when they perform this action (so they do not block the wrong users) you put the button into the UserProfile
component. After all, you have already built that component and you want to keep your application DRY.
import DisableUser from "./DisableUser"
function UserProfile({ user }) {
return (
<div>
{user.fullName}
<DisableUser user={user} />
</div>
)
}
But now everyone sees this button even though solely the people from support should. Coincidentally, another developer already implemented a useIsSupportTeamMember
hook that tells you whether the current user is from your support team. Great, let's use the hook!
import DisableUser from "./DisableUser"
import { useIsSupportTeamMember } from "./hooks"
function UserProfile({ user }) {
const isSupport = useIsSupportTeamMember()
return (
<div>
{user.fullName}
{isSupport && <DisableUser user={user} />}
</div>
)
}
While what we have done might be fine for this use case we can get into trouble if we keep on adding features like this. Next, product management asks for a feature to control feature toggles. Then, marketing asks for a feature to get tracking information about a particular user. Now, the component looks like this.
import DisableUser from "./DisableUser"
import ManageFeatureToggles from "./ManageFeatureToggles"
import ManageTracking from "./ManageTracking"
import {
useIsSupportTeamMember,
useIsMarketingTeamMember,
useIsProductTeamMember,
} from "./hooks"
function UserProfile({ user }) {
const isSupport = useIsSupportTeamMember()
const isMarketing = useIsMarketingTeamMember()
const isProduct = useIsProductTeamMember()
return (
<div>
{user.fullName}
{isSupport && <DisableUser user={user} />}
{isMarketing && <ManageTracking user={user} />}
{isProduct && <ManageFeatureToggles user={user} />}
</div>
)
}
The tricky part is that the code above isn't necessarily bad. However, I would claim that while we intended to re-use the UserProfile
component we ended up re-purposing it. Why? We took four different use cases for a "user profile" and assumed that the overlap in functionality is large enough to build everything into one UserProfile
component. This "master" component now contains code for our end-users, support, product management, and marketing. Since there is little overlap between the use cases most of the code of the component is never executed when we render it.
Initially, this might not cause you trouble. But when requirements change and, for instance, marketing wants more features that don't relate to a user anymore it becomes harder to squeeze them into the existing code. That's when things tend to get ugly and people start to talk about "historic reasons" for certain design decisions.
While you might have heard about coupling before I'm guessing that you haven't heard of afferent and efferent coupling. As I'm the author and I do believe it is worth talking about them we're going to have a closer look in the next section.
Afferent and efferent coupling
A-What? Another tool you can use to check whether your software design goes in the right direction is to check for afferent and efferent coupling. Afferent coupling describes how many components will be affected by a change. Efferent coupling then tells us about how many other components have an effect on the component we're looking at. A trick to remember this is to look at the first letter of the words. Afferent starts with an a like affected and efferent with an e like effect.
The picture above illustrates our current design. Notice that the switches we've put into our business logic do not matter here. Since any component can potentially show up for each group the connection needs to be there. This means that, for instance, the ManageFeatureToggles
component has an afferent coupling value of 4 (because it's used inside the UserProfile
component but also indirectly in ProductAdmin
, SupportAdmin
, and MarketingAdmin
). If we make a change in that component we can potentially affect four others.
Also, the UserProfile
has an efferent coupling value of 3 since it uses three components (ManageFeatureToggles
, ManageTracking
, and DisableUser
). A change in any of the three components could have an effect on the user profile itself.
Again, none of these values are inherently good or bad. But they give us something that we can compare against. Let's look at a different way to structure the code. This time we're going to build dedicated components for each kind of user profile.
In this design, the ProductProfile
has an afferent and efferent coupling value of 1. If we anticipate more changes to the product profile then this design might be more favorable. Why? Because changes are local to the product use case and cannot influence other parts of our application. We have also introduced a new UserDetails
component. This component contains the code that we actually intended to re-use. I would argue that extracting the user details into a small component is the better way to structure our code in this scenario. The new component is small and has no switches which means that all the code (the user information) adds value in every use case where it is rendered. UserDetails
has an afferent coupling value of 6 which isn't bad because we've designed this component to be re-used.
What does this have to do with cohesion? In the first design changes to one use case (e.g. product) would have an effect on other use cases (e.g. marketing). This was reflected in high afferent coupling values on components where we would have expected low values. Since these use cases should not overlap but the components are connected I would argue that we did not achieve cohesion because two components that do not share a use case might influence each other.
The second design approach is different. If we wanted to add another component to the product profile we could implement that without affecting, for instance, any marketing component.
This means that you can achieve cohesion by making sure that components that serve a specific use case have an afferent coupling value which represents this. If you have three components that handle product use-cases but one of those has an afferent coupling value of 10 then this is a smell that you have not achieved cohesion.
Impact on software design
In my experience, the most important thing is to be aware of the concepts I've described above. There are no good and bad values for afferent and efferent coupling per se. For instance, a Button
component will probably have an enormous afferent coupling value but that is totally fine. What is important is to compare the value with the use case you are trying to solve. Is the component meant to be re-used? Then go for it. But make sure that the component covers only one specific use case. A button should handle clicks and a label. You probably don't want to add functionality to do a video call to it because the use case you are currently working on demands it. Especially at the beginning of your career, this can be hard because this requires you to always keep the bigger picture in mind. This is a skill no one will ever master. I still make these kinds of mistakes all the time. While you can point at some part of your application in hindsight and proclaim "This is where we did it wrong" this can be close to impossible when there are other problems to figure out or a deadline is around the corner. My best advice is to stay aware of the issues and constantly challenge your design.
Design to compose
A benefit of keeping use cases separate and making sure that the coupling values represent what we want our components to do will lead to a more composable application design. This is great because it lets you plug together functionality without worrying too much about the reach of your actions. Subsequently, bugs will tend to stay more local. While a bug in a button might still make that button not clickable it won't corrupt your video conferencing. I refer to these as bugs that make sense. The bugs stick to their respective domain and don't lead to a whack-a-mole kind of situation.
Another big benefit is that high cohesion makes your software easier to maintain. If you discontinue a part of your application you can be sure that you might have to fix the occasional bug but you will not need to adjust the legacy code each time you add a new feature.
Even though this means that we need to spend a little more time thinking about our software design it can save ourselves time and trouble in the future. And that's why you should care about cohesion.
Update 2020-09-01
It got pointed out to me that it might be helpful to include the changed code for the specific profile components as well. Here we go. What you will end up with are more but smaller components without conditionals in them.
Product profile
import ManageFeatureToggles from "./ManageFeatureToggles"
import UserDetails from "./UserDetails"
function ProductProfile({ user }) {
return (
<>
<UserDetails user={user} />
<ManageFeatureToggles user={user} />
</>
)
}
Support profile
import DisableUser from "./DisableUser"
import UserDetails from "./UserDetails"
function SupportProfile({ user }) {
return (
<>
<UserDetails user={user} />
<DisableUser user={user} />
</>
)
}
Marketing profile
import ManageTracking from "./ManageTracking"
import UserDetails from "./UserDetails"
function MarketingProfile({ user }) {
return (
<>
<UserDetails user={user} />
<ManageTracking user={user} />
</>
)
}
Top comments (0)