Coupling and cohesion are paired concepts of software quality. The basic ideas were laid out by Larry Constantine as long ago as 1968, and developed broad audiences with the rise of structured programming in the late 1970s. But “loosely coupled” is a ubiquitous virtue in conversation: “Why is dependency injection good?” “Loose coupling!” “Why is Model-View-Controller good?” “Loose coupling!” “Where do you want to go for lunch?” “Loose coupling!” Meanwhile, cohesion is a largely forgotten virtue.
Cohesion is the degree to which a software module’s behaviors form a meaningful whole. Higher internal cohesion is good: the things you need to perform a task are localized. In mainstream programming languages, classes and namespaces should be highly cohesive.
Loose coupling lives in a certain tension with cohesion. Cohesion is great for the task at hand, but loose coupling, the degree to which dependencies between code modules is minimized, is important for extension. Cohesion is a way to judge the surface area of a module; low coupling is something you should look for in the way your module’s surface interacts with others. As you go up and down in abstraction, you have to shift your level of attention: When you’re working within a module, you don’t want to repeat code or behavior for the sake of avoiding a function call or a useful abstraction.
At the higher level of modules, I see too much code that is overworked in service to some mythical day when the underlying implementation will be “swapped out.” It’s a virtuous ideal, but when you start digging into the module structure, you find that actually getting a computation done requires five different modules. One’s hands are tied when it comes to “swapping out” just one of the implementations because of the lack of cohesion. If you have some rework goal, such as parallelization or performance, it turns out that the dependencies between the modules are “loose” only in the trivial sense that they are determined at runtime.
When working at the level of namespaces or libraries, I think of cohesion in terms of tabs: How many tabs do I have to have open to comprehend what’s going on in the code or what my options are? One of object orientation’s great benefits is that it provides a model that promotes “informational cohesion”: a set of functions on a single data structure. One naturally expects the function for string length in the String class, and scanning a class’ methods should give a sense of operations on that data.
Object orientation’s cohesion-promoting premise has been diluted by the meme of “composition over inheritance” and the quiet rise of functional programming techniques. This has led to an explosion of classes in our designs and less—not more—clarity. The pendulum has swung so far away from diagram-producing “architecture astronauts” toward “shut up and code” coding ninjas that it’s gotten to the point where pondering (much less drawing up and discussing) design and architecture alternatives is rare. (When was the last time you saw an article on programming with a class or sequence diagram?)
John Cook says that where once we passed a bread data structure to a slice method, and then we had the object-oriented bread slice itself, we’ve now “refined” things to the point where it’s “We create a bread-slicing object, and then we simply pass bread objects to the slice method on the bread-slicer.”
True, if the creation of classes is sloppy and unguided. But in service to the ideal of “functional cohesion,” the most favored level of cohesion, in which every element in the module works together to perform exactly one goal and everything not related to that goal is excluded, then Cook’s criticism is too strong. The “bread-slicing module” ought not, in this view, be bogged down with the computations of buttering or toasting or other bread-related functions.