When describing stability of software components, there’s two common meanings that the word “stability” takes on:
- A component’s need to change. Does the business require it to change?
- A component’s resistance to change. How difficult would it be to change it?
In the book Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert Martin (Uncle Bob), uncle bob introduced one way of quantifying a components intrinsic resistance to change and calls it the Stable Dependencies Principle (SDP).
The SDP is a measure based strictly on source code dependencies. By source code dependencies, he basically means literal name references to other components such as imports.
How does it work?
When a component X imports component Y, then component X depends on Y. The dependency “fans out” from X to Y. If component X imports 5 other components, then the fan out count is 5.
When a component X is imported by component Y, then Y depends on X and the dependency “fans in” from Y to X. If component X is imported by 5 other components, then the fan in count is 5.
The formula for calculating the “stability” score is:
(fan out / fan in + fan out). You get a score between 0 and 1. 0 if there are no fan-outs (maximally stable). 1 is there are only fan outs (maximally unstable).
If a component has a high fan out count, then it has many reasons to change since any change in the things it depends on may require itself to change. It has no control over others. Therefore, it is unstable by the definition of the SDP.
On the other hand, if a component has a high fan in count, then it has many reasons not to change - lots of components depend on it to continue working as is.
While I think this is a good basic formula to apply (counting direct imports), it doesn’t work perfectly for some of the most insidious types of unwanted dependencies I’ve encountered. These are common ones that are, while not impossible to measure, harder to discern with the naked eye.
Indirect source dependencies
A component X may not dependent on Y through a direct “import Y” statement, but it uses “Y” (maybe it’s returned by a collaborator).
How to spot: lots of references to object instances, unit tests require lots of mocking even though source code dependency count is low
Data moves through the system and at every stage it’s expected to take on some form. Data isn’t source code, but there are expectations of what form that data needs to take on that add rigidity to your code. Therefore, components that depend on another component for data is more unstable then one that depends on another component but does not rely on its data.
How to spot: pretty much any return value from another component that gets used for iteration, key access, etc.
Data dependencies V2
There’s another really subtle form of data dependency that not only adds rigidity, but also lots of frustration when that data contract changes. It’s when the data contains assumptions or implicit knowledge of the consumers of that data.
For example, an API that returns data withs keys formatted in a specific way for a single consumer. Maybe that consumer can only work with odd numbered keys for whatever reason (bad example, but you get the point).
At a glance, this looks like a normal data dependency - but assumptions about the clients are built in. When things start failing for the specific client you built this dependency for, you’re going to have a hell of a time tracking down that failure.
How to spot: data needs to be structured in a particular way or a a client just won’t work, pretty much any “weird” decisions in the data structure that have nothing to do with strictly data organization. For example, having a bunch of duplicate data in order to simplify logic in another layer of the codebase.
Sometimes, you have components that pass data containing references to components. In other words, data that contains objects instead of just plain data structures. One example is from a rails code base I currently work on where a list of active record model instances are passed directly to the view to be used for some code generation.
If you look at the views themselves, you can’t tell that AR models are being used. I mean, there’s no apparent source code dependency right?
But surprise, they are! The more the view depends on the state of those models, the more coupling you now have between the view and model layers of your application. For example, maybe some random attribute on a model gets deleted and now all the view code is failing.
So many dependencies so little time
I’ve never really thought deeply about dependencies until much later into my career when I grew increasingly frustrated over the difficulty to change code as codebases grew. Often times, that frustration comes hardest when there’s a business need to change coupled with components that need to change but are maximally resistant to change. For some sick reason, those times are also the times when the business has no appetite for refactoring work.