Dependency inversion does not simplify the design
After coming up with the code pattern (for adding compile time mocks/stubs/fakes) described in my previous blog post I started to think about system architecture more broadly. One thing which becomes readily apparent is this dependency inversion stuff can only be to facilitate testing. Once it becomes possible to implement the same testing without writing an interface (and using run-time poly-morph-ism) it becomes obvious that the implementation without the interface is the simpler of the designs.
To make this concrete take this simple software design where some Behaviour type requires access to a Storage type. Figure 1 shows this in UML notation. The behaviour type probably implements some business rule and the storage implements the sql query code. The dashed line represents a significant component boundary like a package boundary which is considered significant.
Figure 2 shows a typical implementation of the same thing using the dependency inversion pattern. The result is that all the dependencies now run up the diagram.
To give the idea another go however maybe maybe the re-organisation has ordered the design along a significant axis. We know ideally what axis that is and that it runs from the least fluid parts of the design towards the most fluid parts of the design. In this case we would have simplified the future iterations of the design as the more fluid parts change together at a similar rate and the less fluid parts change at a similar rate. But even if we can assume that the axis runs most fluid to least fluid upwards in these diagrams this conclusion doesn't follow. In this case if we change the implementation of Storage in a well encapsulated way then already we don't have to change the user behaviour in both implementations. On the other hand if the interface to storage needs to change in a way effecting Behaviour then the only difference with the figure 2 implementation is that IStorage also needs to be changed together, and the implementation of Behaviour needs to be changed to use the new interface.
In fact the same reasoning applies to any arbitrary axis of organisation you can organize components with. Previously we would order them along some axis running from the interface down to the storage medium. This is at least reasonably well defined and likely to lead to coupled components changing together, but can be problematic if you can't, or eventually can't, test the software components isolated from their storage mechanism. Other axis can be harder to conceptualize and may make it difficult for teams to agree on where a component should reside on a particular axis. Regardless of what order you impose however the dependency inversion pattern variant doesn't improve the design coupling. What improves the coupling is the encapsulation of the components making up the design.
So in summary
![]() |
| Figure 1 |
To make this concrete take this simple software design where some Behaviour type requires access to a Storage type. Figure 1 shows this in UML notation. The behaviour type probably implements some business rule and the storage implements the sql query code. The dashed line represents a significant component boundary like a package boundary which is considered significant.
![]() |
| Figure 2 |
Figure 2 shows a typical implementation of the same thing using the dependency inversion pattern. The result is that all the dependencies now run up the diagram.
It should be apparent that Figure 1 is the simpler implementation of this code. It has 2 types rather than 3 and 1 relationship rather than 2 relationships. Further what relationship exists between the Behaviour and Storage types has just been spread further apart in Figure 2 by adding a level of indirection with the intermediate type IStorage. So in concrete terms, we are adding complexity to the design to facilitate testing. This is true, but something is missing from these diagrams, and its always missing from similar diagrams, these diagrams don't even bother to mention how the testing interacts with the behaviour and its silently driving the design. I think this is another way of describing what David Heinemeier Hansson called test- induced design damage.
To give the idea another go however maybe maybe the re-organisation has ordered the design along a significant axis. We know ideally what axis that is and that it runs from the least fluid parts of the design towards the most fluid parts of the design. In this case we would have simplified the future iterations of the design as the more fluid parts change together at a similar rate and the less fluid parts change at a similar rate. But even if we can assume that the axis runs most fluid to least fluid upwards in these diagrams this conclusion doesn't follow. In this case if we change the implementation of Storage in a well encapsulated way then already we don't have to change the user behaviour in both implementations. On the other hand if the interface to storage needs to change in a way effecting Behaviour then the only difference with the figure 2 implementation is that IStorage also needs to be changed together, and the implementation of Behaviour needs to be changed to use the new interface.
In fact the same reasoning applies to any arbitrary axis of organisation you can organize components with. Previously we would order them along some axis running from the interface down to the storage medium. This is at least reasonably well defined and likely to lead to coupled components changing together, but can be problematic if you can't, or eventually can't, test the software components isolated from their storage mechanism. Other axis can be harder to conceptualize and may make it difficult for teams to agree on where a component should reside on a particular axis. Regardless of what order you impose however the dependency inversion pattern variant doesn't improve the design coupling. What improves the coupling is the encapsulation of the components making up the design.
So in summary
- the dependency inversion pattern is for, and only for adding testing seams to the design.
- adding testing seams to a design often increases the complexity of that design.
- there is no natural axis along which a design is oriented.
- to reduce coupling in a design improve the encapsulation of the components making up the design and reduce the coupling to implementation details.


Comments
Post a Comment