Back to Basics: Cohesion and Coupling Part 2
This post is a continuation of my post on cohesion and coupling, it is part of a series of back to basics posts examining and questioning some of the core principles and practices of software development.
In my last post I talked about what cohesion and coupling are and I talked about some of the benefits of each.
Now I want to take a look at the actual application of cohesion and decoupling on software systems.
Granularity affects cohesion and coupling
Here is the key misunderstanding with cohesion and decoupling.
Cohesion and decoupling are completely relative to the granularity of what is a module.
From here on out, I won’t say class or software or system, I am going to say module when referring to cohesion and coupling.
See, the problem is that we need to be able to define what a module is in order to determine if something is loosely coupled or highly cohesive.
Let me give you an example. Let’s take that linked list class we talked about above. If we define a module to be any class in our code, then it is pretty decoupled. But, if we define a module to be any class or primitive type in the system, suddenly our linked list implementation is going to be dependent on many other things. Now all the variables we declare in our class are dependencies on things like the Array and the String class or integer implementations.
When we zoom deep down into the above level of what a module is we end up with many more dependencies which represent tighter coupling.
What about cohesion? Consider if you will, Enterprise FizzBuzz. This is an implementation of the FizzBuzz problem:
- Print the numbers from 1 to 100
- If the number is divisible by 3 print “Fizz” instead
- If the number is divisible by 5 print “Buzz” instead
- If the number is divisible 3 and 5 print “FizzBuzz” instead
It is an implementation of this simple problem using 3 assemblies and 16+ classes. If we consider a module to be a class, it is not very cohesive at all, since the responsibilities of what should be in a single method or two are spread out across 16. If we consider a module to be an assembly, it still isn’t very cohesive.
We have to zoom all the way out to the module is a program level before this software becomes cohesive, but at that level it is very coupled to all the the various other frameworks that it depends on.
There are two important takeaways from this section:
- How we define a module when looking at software affects cohesion and coupling.
- The granularity we use to build the software affects cohesion and coupling. (This one was hidden in the enterprise FizzBuzz example. In this case the author of the code defined a responsibility to be something very very small.)
To summarize, we can look at code from different levels of zooming in and out and determine its coupling and cohesiveness. We can also build software as different size “Lego blocks” which has the same effect.
When cohesion and coupling are inversely related
With that background, we can finally answer the question of whether or not it is possible to achieve high cohesion and loose coupling.
The answer is “to a degree.”
If we try to push too far into the loose coupling zone, we will find that we end up making our definition of a responsibility very very small at which point we lose the quality of cohesion.
I’m going to pick on overuse of interfaces again to give you an example.
Consider what happens when we create an interface to “reduce coupling” so that we can create unit tests. We end up decreasing cohesion because the class our class was referencing (one jump), now is an interface which is implemented by a class (two jumps.)
Now consider what happens when we add a dependency injection module or even just a factory to get the implementation. Our once simple and highly cohesive implementation is spread out across an interface which is implemented by a class that we have to look up in a factory which contains some sort of a mapping file to map the interface to the implementation. (3-4 jumps.)
I know this concept seems very strange, but let me see if I can explain it with a real world example.
Consider again the highly cohesive baseball. It’s already at the optimum coupling of 0. It doesn’t depend on anything else. But, if we wanted to we could try to decouple it. How? We have to zoom in to a point where we could consider that the outer stitching is coupled to the casing which is coupled to the inner ball.
We could decouple that baseball by taking it apart and design some kind of interface which allows for different kinds of binding mechanisms for the outer shell and some kind of substrate to prevent the outer shell from directly touching the inner ball.
If we did that we’d have pieces of the ball lying all over the place and it wouldn’t be very cohesive or even functional.
We can do that exact thing with software. There is a point where we have achieved the maximum qualities of loose coupling and high cohesion, and we can try to push decoupling at the cost of cohesion.
Cohesion is more important
It probably appears that I am saying that cohesion is more important than decoupling, and actually I am.
If you remember the advantages of tight cohesion and loose coupling, you might have realized that most of the advantages are the same except tight cohesion gives us the benefit of increased understanding.
I tend to value understanding and simplicity in software above most other things, because they aid the most in maintenance and debugging.
It is very important that we consider cohesion when we seek to increase decoupling. It also can help give us a very clear measure of when we have maximized decoupling. At the point where any more decoupling will harm cohesion, we are done.
So is dependency injection bad?
No, not at all. Dependency injection and other methods of decoupling have their places, but it is very important that we don’t just blindly use them for every class in every situation.
We have to be conscious of what we are losing in cohesion and understandability when we consider using any kind of framework or pattern to decouple our software.
I’ll pick on one more thing here, since I think it is not very obvious. Consider how good a message bus can be for integrating different applications together. Messaging systems can decouple the different applications that need to communicate with each other making them highly cohesive and very loosely coupled.
Now, consider how bad a message bus can be inside of an application. I know it is a fairly popular solution for decoupling events, commands, and other communication within an application, but many times the cost of the decoupling is a very high hit to cohesion and understandability.
Don’t get me wrong, sometimes an internal message bus is a good solution for an application, but in many cases it is going to hurt you more than help you.
It’s all about right sized Lego blocks
Scott Hanselman often likes to talk about “right sized Lego blocks”, and I agree with him 100%. Figuring out how to appropriately decouple your application while maintaining cohesion is all about figuring out what the ideal size of a module is.
Sometimes the answer might even be to split your application into multiple applications.