Cohesion and coupling
◈ 7 cardsThe two qualitative criteria that assess functional independence: the cohesion ladder inside a component, the coupling categories between components, and why lowering one naively wrecks the other.
The property, and the two measures that assess it
Functional independence is achieved by building modules that each do one job and that reach outside themselves no more than that job actually requires. It is not measured directly. It is assessed using two qualitative criteria, and this is the sentence that anchors both definitions: cohesion looks inside one component, coupling looks between components.
Both are qualitative measures. Neither is a count you compute and neither is a defect you find; they are properties that increase or decrease as a design changes, and they predict the same downstream costs — how hard the system will be to implement, to test and to maintain.
Cohesion — how single-minded one component is
Cohesion asks whether a component holds everything one job needs and nothing else: every attribute and every operation inside it should be there because that single responsibility requires it. The levels run best-first.
- Functional — the component performs one and only one computation and returns a result. This is the target.
- Layer — a higher layer uses the services of a lower one, and lower layers never reach upward. Exhibited by packages and components rather than single operations.
- Communicational — every operation that touches the same data lives in one class, which then focuses solely on that data.
Below those sit weaker rungs — sequential (one operation's output feeds the next), procedural (operations grouped because they run in a fixed order), temporal (grouped because they happen at the same time, such as start-up), and utility (grouped because there was nowhere else to put them). Functional, layer and communicational cohesion make a component easy to implement, test and maintain, and you should reach for them. Pragmatics sometimes force a lower level; that has to be a decision you can defend, not something that happens to your design while you are not looking.
Coupling — how entangled components are with each other
Coupling is a qualitative measure of the degree to which classes or components are connected to one another. As components become more interdependent, coupling increases; as coupling increases, so does the difficulty of implementing, testing and maintaining the software, because a change in one place forces a change or a retest somewhere else. The objective is always to keep coupling as low as the problem allows — not zero, because software must communicate, but low.
The categories, worst first:
- Content coupling — one component reaches in and modifies data that is internal to another. It violates information hiding outright, and it means the second component can no longer be reasoned about on its own.
- Control coupling — one operation passes another a flag that directs the callee's internal logic. The two are now bound by a shared private meaning, so an unrelated change inside the callee can silently invalidate what the flag means.
- External coupling — a component talks directly to infrastructure: the operating system, a database, a telecoms or payments service. This kind is necessary, so the design rule is not to eliminate it but to confine it to a small number of components.
- Data and stamp coupling — communication by parameters only. Stamp coupling passes a whole record where a field would do; data coupling passes just what is needed. This is where you want to be.
They trade against each other
The pair is not two independent dials. Merging components to reduce the number of connections between them lowers coupling and destroys cohesion. Splitting a component to raise cohesion adds connections and can raise coupling if you split on the wrong axis. The judgement is in choosing the axis: split along responsibility boundaries and both improve; split along convenience boundaries and both get worse.
Worked example — the late-fee call
BorrowBox's locker estate carries eleven sensor classes across three hardware generations — door sensors, weight pads, scan pads, tamper switches. Each of them can detect the condition "this tool came back late". The tempting design gives every sensor a chargeLateFee() operation that calls the Payments component directly. It works, it is the shortest path, and it is wrong.
Count what it costs. Eleven classes are now coupled to billing, and coupled externally through billing to a third-party payments gateway. Change the fee schedule — say the first two hours become free — and you edit and retest eleven classes. Worse, each sensor now holds a fragment of business policy it has no business knowing, so nobody can say where the fee rule lives.
Route it instead. Every sensor publishes a plain observation: ToolReturned(copyId, at). LendingService subscribes, compares the timestamp against the loan's due time, decides whether a fee is owed, and asks Payments. Now exactly one component is coupled to billing, the sensors know only about sensing, and the fee change is a one-file edit with one set of tests.
The same example shows cohesion. Somebody proposes merging renderDoorState(), renderQueueDepth() and renderBatteryBar() on the locker-bank tile into a single renderTile() — three small operations replaced by one, less code, apparently simpler. It destroys functional cohesion: renderTile() no longer does one thing, so a change to the battery display can break the door-state display, no part of it can be reused alone, and the support engineer who touches it in two years has to understand all three concerns to change one.