Modularity & Coupling
Modularity & Coupling
Modularity determines how well a system can be understood, changed, and maintained. Well-modularized systems have high cohesion within components and low coupling between components. Understanding how to measure and manage these properties is fundamental to architectural thinking.
Cohesion: What Belongs Together
Cohesion measures how closely the elements within a module are related. High cohesion means everything in the module belongs together because it serves a unified purpose. Low cohesion means the module contains unrelated elements that happen to be grouped together.
Computer scientists Larry Constantine and Edward Yourdon identified seven levels of cohesion, ranked from best to worst:
1. Functional Cohesion (Best)
The module performs a single, well-defined task. All elements contribute to that single task. This is the gold standard.
Example: A PaymentProcessor class that validates payment information, communicates with a payment gateway, and records the transaction. Every method serves the single purpose of processing payments.
2. Sequential Cohesion
The module’s elements form a processing chain where the output of one element becomes the input of the next.
Example: A data pipeline module that reads a file, parses the contents, validates the data, and writes it to a database. Each step feeds into the next.
3. Communicational Cohesion
Elements operate on the same data or contribute to the same output, but don’t form a strict sequence.
Example: A report module that reads customer data and produces multiple report formats (PDF, CSV, JSON) from that same data.
4. Procedural Cohesion
Elements are grouped because they execute in a specific order, even though they serve different purposes.
Example: A startup module that initializes logging, loads configuration, connects to the database, and starts a web server. These are related by timing but serve different functions.
5. Temporal Cohesion
Elements are grouped because they execute at the same time, with little other relationship.
Example: An initialization module that sets up unrelated systems (logging, caching, messaging) just because they all happen at startup.
6. Logical Cohesion
Elements are grouped because they’re logically categorized together, even though they serve different functions.
Example: A Utilities class containing string formatting, date manipulation, and file operations. These are logically “utility functions” but functionally unrelated.
7. Coincidental Cohesion (Worst)
Elements have no meaningful relationship. They’re grouped together arbitrarily.
Example: A Helpers class containing random functions that don’t fit anywhere else. This is a code smell indicating poor design.
Coupling: Dependencies Between Components
Coupling measures how much one component depends on another. Low coupling means components can change independently. High coupling means changes ripple across multiple components.
Measuring Coupling
Two primary metrics quantify coupling:
Afferent Coupling (Ca) counts the number of components that depend on this component. High afferent coupling means many components rely on this one, making it harder to change without breaking dependents.
Efferent Coupling (Ce) counts the number of components this component depends on. High efferent coupling means this component is fragile—changes to any dependency can break it.
Derived Metrics
Abstractness (A) measures the ratio of abstract elements (interfaces, abstract classes) to concrete elements in a component:
A = Abstract Elements / Total Elements
A = 0 means purely concrete implementation. A = 1 means purely abstract interfaces. Higher abstractness generally indicates more flexibility.
Instability (I) measures how likely a component is to change based on its coupling:
I = Ce / (Ce + Ca)
I = 0 means maximally stable (high incoming dependencies, low outgoing dependencies). I = 1 means maximally unstable (low incoming dependencies, high outgoing dependencies).
Distance from Main Sequence (D) balances abstractness and instability:
D = |A + I - 1|
Components should fall along the “main sequence” where abstract, stable components (high A, low I) and concrete, unstable components (low A, high I) both have D values near zero.
Zone of Uselessness: High abstractness, low instability. The component is abstract but nobody depends on it. It’s over-engineered.
Zone of Pain: Low abstractness, high instability. The component is concrete and rigid, yet many other components depend on it. Changes are painful and risky.
Connascence: A More Precise View of Coupling
Concept introduced by Meilir Page-Jones (1992), popularized in software architecture by Jim Weirich and Kevin Rutherford
Connascence describes coupling more precisely than simple metrics. Two components are connascent if changing one requires changing the other to maintain correctness. Understanding connascence types helps identify where coupling exists and how to reduce it.
Static Connascence (Source-Code Level)
Connascence of Name (CoN): Components must agree on entity names.
When you rename a method, you must update all call sites. This is the weakest form of connascence and is easily managed by refactoring tools.
Connascence of Type (CoT): Components must agree on data types.
Changing a parameter from int to string breaks all callers. Type systems catch these issues at compile time, making this connascence relatively safe.
Connascence of Meaning (CoM): Components must agree on the meaning of values.
If one component interprets status = 1 as “active” and another interprets it differently, the system breaks. Magic numbers and boolean flags often create this problem. Use enums or explicit types to eliminate meaning connascence.
Connascence of Position (CoP): Components must agree on parameter order.
calculateTotal(price, tax) vs calculateTotal(tax, price) creates subtle bugs. Modern languages support named parameters to eliminate position connascence.
Connascence of Algorithm (CoA): Components must agree on a particular algorithm.
Encryption and decryption must use the same algorithm. Hash generation and validation must match. This connascence is often necessary but should be isolated to single locations.
Dynamic Connascence (Runtime Level)
Connascence of Execution (CoE): Order of execution matters.
You must call connect() before sendData(). This connascence is common but dangerous because compilers can’t verify it. Use state machines or builder patterns to enforce correct ordering.
Connascence of Timing (CoT): Timing of execution matters.
Two threads accessing shared state without synchronization create race conditions. This is one of the strongest and most dangerous forms of connascence. Use locks, atomic operations, or message passing to eliminate timing connascence.
Connascence of Values (CoV): Multiple values must change together.
Updating width requires updating height to maintain aspect ratio. Updating a user’s email requires updating their authentication record. Use transactions or aggregates to ensure values change atomically.
Connascence of Identity (CoI): Components must reference the same entity.
Multiple services must point to the same user record. Distributed systems struggle with identity connascence when entities are replicated. Use unique identifiers and eventual consistency patterns carefully.
Properties of Connascence
Three properties help evaluate the severity of connascence:
Strength: How difficult is it to refactor? Name connascence (weak) is easier to fix than timing connascence (strong). Stronger connascence creates more coupling and is harder to change.
Locality: How close are the connected components? Connascence within a single class is less problematic than connascence across services. Distance amplifies the impact of connascence.
Degree: How many components are affected? Connascence between two components is manageable. Connascence affecting dozens of components is a serious design problem.
Improving Modularity
Three principles guide connascence improvement:
- Minimize overall connascence by reducing unnecessary dependencies
- Minimize connascence across architectural boundaries by keeping strong connascence local
- Maximize connascence within boundaries by allowing high cohesion within modules
Apply these strategies:
Convert strong connascence to weaker forms: Replace position connascence with name connascence using named parameters. Replace meaning connascence with type connascence using enums.
Isolate connascence: Move strongly connascent code into the same module. If execution order matters, encapsulate the sequence within a single component.
Reduce degree: When multiple components share connascence, refactor to reduce the number of affected components. Extract shared logic into a single module.
Respect boundaries: Allow strong connascence within a module but enforce weak connascence across module boundaries. Timing connascence within a service is acceptable; timing connascence across services is dangerous.
Found this guide helpful? Share it with your team:
Share on LinkedIn