Acyclic Dependencies Principle
Dependency hierarchy
- Definition
- Allow no cycles in the module dependency graph
How does it work?
Most programming languages have an import function that allows one module to use the code from another module. These imports together form a graph.
Graphs can form cycles and the same goes for module imports, but in general it's a good thing to strive for unidirectional dependencies: they provide conceptual clarity and make code easier to maintain.
There is a principle that says that such a graph should not have any cycles, (or: circular dependencies). That is, if module A depends on B and B on C, then C should not depend on A.
When should you use it?
There are some practical reasons when circular dependencies are not possible:
- If the modules need to initialize resources before they can be used, circular dependencies lead to a chicken-and-egg problem. If the database module needs the logger module to write the startup time of the database, but the logging module needs the database to find out where to write the log.
- If the code is written in a language that denies circular dependencies
And there are some objections to circular dependencies:
- Testing and maintenance: a change in module A means that all modules that depend on A may have been broken by the change and should be tested again. The further the dependency points upward, the more important the objection holds.
- Circular class references create high coupling; both classes must be recompiled every time either of them is changed
- Circular assembly references prevent static linking, because B depends on A but A cannot be assembled until B is complete
- Circular object references can crash naïve recursive algorithms (such as serializers, visitors and pretty-printers) with stack overflows. The more advanced algorithms will have cycle detection and will merely fail with a more descriptive exception/error message.
- Circular object references also make dependency injection impossible, significantly reducing the testability of your system.
- Objects with a very large number of circular references are often God Objects. Even if they are not, they have a tendency to lead to Spaghetti Code
- Circular entity references (especially in databases, but also in domain models) prevent the use of non-nullability constraints, which may eventually lead to data corruption or at least inconsistency.
- Circular references in general are simply confusing and drastically increase the cognitive load when attempting to understand how a program functions.
Examples
- The language Go enforces explicit dependencies and disallows circular dependencies
Problems
- A circular dependency may be hard to resolve