This chapter discusses 3 principles dealing with component coupling and how they can be balanced to build maintainable systems
Acyclic Dependencies Principle
"Allow no cycles in the component dependency graph"
A common issue developers face is trying to constantly integrate their changes on top of others, usually due to working on the same files and causing merge conflicts
Essentially this repetitive process can cause the "morning after syndrome", where being able to build a stable version of a project becomes much more difficult
Two solutions have evolved to address this issue: "The weekly build" and the Acyclic Dependencies Principle (ADP)
The Weekly Build
How it works:
Developers ignore each other for the first 4 days of the week, working on private copies without worrying about integration
On Friday, they integrate all their changes and build the system
The approach allows developers to freely do as they please at the start with the incoming penalty of integrating potentially large changes
However, as the project grows, the integration phase becomes less feasible
Developers are either forced to work extra on Saturdays or begin earlier
Eventually, integration time increases which reduces development time, overall reducing efficiency
Eliminating Dependency Cycles
Solution: Partition development environment into releasable components
Components become the units of work that a single or team of developers can be responsible for
Components can be released with a release number for other developers to use
Other teams aren't immediately affected by new components or required to use it, so friction between teams is non-existent or minimal
Developers must manage the components dependency structure properly, if there are cycles within it then the "morning after syndrome" can't be avoided
Example of component dependency structure in Figure 14.1:
Structure is a directed graph
Components are nodes
Dependency relationships are directed edges
The structure has no cycles
Impossible to begin at one component and follow the dependencies to wind back up to that component
This type of graph is called a directed acyclic graph
Consider the Presenters component:
If Presenters has an update, View and Main who depend on that component will need to decide when to integrate their work with the new release
When testing and building Presenters, they need to only build it with the Interactors and Entities components that it depends on
No other components in the system need to be involved, reducing the impact of these changes
When the whole system is released, it builds the components bottom up based off the dependencies between them
The Effect of a Cycle in the Component Dependency Graph
In Figure 14.2, Entities now depends on Authorizer (User class uses Permissions class from different components)
This creates a dependency cycle between Entities, Authorizer and Interactors
Essentially, these three components become one LARGE component
Releasing Database means building this large component, which now includes the new dependency, Authorizer
With more areas of the system affected, developers will begin to experience the same issues from before
This level of coupling can cause multiple issues:
Building multiple components as a result of more dependencies
Difficult to isolate components
Difficult to do unit testing and more risks of releasing
Build issues increasing with the number of modules and the order to build components is unclear
Breaking the Cycle
Two primary mechanisms for breaking the cycle of dependencies:
Apply the Dependency Inversion Principle
Ex. adding an interface between Entities and Authorizer to invert the dependency between them
Create a new component that both Entities and Authorizer depend on
Move the classes User and Permissions into the new component
This solution of creating new components grows the dependency structure and makes it volatile
Important to always monitor for cycles and to always break any cycles that occur
Top-down Design
Component structure can't be designed from the top down
Component dependency diagrams don't describe the application's function, but the buildability and maintainability of the application
This diagram does not exist at the project's beginning, since there is no software to maintain at that point
The necessity to manage dependencies grows as more and more modules accumulate
Focusing more on SRP and CCP to group classes that are likely to change together
This structure also protects frequently changing components from affecting stable components
Ex. changes to GUI shouldn't impact business rules or modification of reports to impact high-level policies
As the application grows, we become more concerned with reusable elements, thus, CRP begins to influence the composition of components
The component dependency structure grows and evolves with the logical design of the system
Must be aware of reusable elements otherwise we might create components with dependency cycles
Stable Dependencies Principle
"Depend in the direction of stability"
Stable components should not depend on volatile components
Otherwise, it will be difficult to change the volatile component
Volatility is necessary for a maintainable design, it cannot be completely static
By applying CCP, we create components sensitive to certain changes but immune to others
Some components are designed to be volatile
Stability
Stability is measured by the amount of work required to make a change
Ex. a coin is not stable just because it can maintain its position on its side, rather its unstable because little work is required to topple it
A component is stable if it is hard to make a change
A component which is depended upon by lots of components is stable because any changes made to it will require reconciliation with all dependent components
In Figure 14.5, component X has 3 reasons to not change as 3 components depend on it
In Figure 14.6, component Y is unstable, its changes may come from any of the 3 components it depends on
Stability Metrics
Positional stability of a component: Number of dependencies entering and leaving a component
Fan-in (incoming): No. of classes from other components that depend on classes within the component
Fan-out (outgoing): No. of classes within the component that depend on classes outside the component
I (instability): Has the range [0, 1] calculated from Fan-out / (Fan-in + Fan-out).
A lower value indicates more stability, with 0 meaning no outward dependencies
A higher value indicates high unstability, with 1 meaning no inward dependencies and at least 1 outward dependency
In programming languages, dependencies are typically represented by import statements
SDP states that the I metric of a component should be larger than the I metric of the components it depends on
I metrics should decrease in the direction of dependency
Dependant components are more likely to change (more unstable)
Not All Components Should Be Stable
All components being maximally stable means a system would be unchangeable
Ideally, some components are unstable and others are stable
The unstable components should depend on stable components
Useful convention in diagrams is to put unstable components above since any visible dependency arrow going upwards shows a clear violation of SDP
In situations where a stable components depends on an unstable component, apply DIP
In Figure 14 and 14.11:
Class u in the Stable component depends on class c in Flexible component
Solution: Create another component UServer with an interface class called us which declares all methods u needs to use. c then implements this interface, breaking the dependency of the Stable and Flexible components
Dependencies also now flow in the direction of decreasing I metric, with the new component having I = 0
Creating abstract components (no executable code, contains only an interface) is common and they're very stable, making them ideal targets for less stable components to depend on
Stable Abstractions Principle
"A component should be as abstract as it is stable"
High-level policies and business rules within a software system should not be volatile
Should be placed in stable components
However, the source code representing these policies become hard to change
The stable component needs to be flexible enough to withstand change
OCP shows functionality can be extended without requiring modification of other classes
SAP is a relationship between stability and abstractness
Stable components should be abstract such that stability does not prevent it from being extended
Unstable components should be concrete since its instability allows the concrete code within it to be easily changed
Implies that a stable component should consist of interfaces and abstract classes
DIP for components is SAP and SDP combined
Dependencies run in the direction of abstraction
DIP deals with classes, which are either abstract or not
SAP and SDP combined deal with components, which can be partially abstract and partially stable
Measuring Abstraction
A metric measures the abstractness of components
The ratio of classes and combined interfaces and abstract classes within a component
Nc: No. of classes
Na: No. of abstract classes and interfaces
A: Na / Nc, it ranges from [0, 1] with a higher value indicating more abstractness of the component
The Main Sequence
The relationship between stability (x-axis) and abstractness (y-axis) can be depicted on a graph
Maximally stable components sit at (0, 1)
Maximally unstable components sit at (1, 0)
Components have varying degrees of abstraction and stability, so they'll rarely sit at the end points
Instead the graph can consist of a locus of points that define reasonable points for the components to reside
The Zone of Pain
Components at (0, 0) are concrete and high stable, but it has no flexibility for withstanding change
The area around this point is referred to the Zone of Pain
Examples:
Database schemas are concrete and highly depended on, making updates difficult
Utility library such as String, very commonly used it's considered non-volatile
The Zone of Uselessness
Components at (1, 1) are abstract and are not depended on, essentially they are useless components
Entities within this area are usually unused code or forgotten abstract classes that aren't implemented
Avoiding the Zones of Exclusion
Clearly components should avoid either zones around (0, 0) and (1, 1)
The locus of points maximally distant from each zone is the connecting line from (0, 1) and (1, 0)
Referred to as the Main Sequence
Components around this line are not too abstract, not too unstable, not too useless and not too painful
Ideally, components sit at either end points or close to them
Distance From the Main Sequence
The D metric measures how far a component is away from the Main Sequence
Calculated by | A + I - 1 |, using the abstractness and instability metrics
The range is [0, 1] where a value of 0 means the component is on the Main Sequence
Thus, any component with a D value not near 0 can be re-evaluated
Statistical analysis to calculate variance and identify the "best" components for examination is also possible
Can also be useful to examine the change in D over releases, ensuring the component dependency tree is consistently managed
Conclusion
The dependency management metrics describe a pattern of dependency and abstraction for components shaped by the three principles
Acyclic Dependencies Principle: Dependencies shouldn't form cycles, changes are harder to make
Stable Dependencies Principle: Less stable modules should depend on more stable modules
Stable Abstractions Principle: The more stable a module is, the more abstract it should be to enable flexibility
Don't treat metrics as an absolute, but they are generally good guidelines to follow