Low coupling between objects is a key principle to help you win the battle against software entropy. Making sure your dependencies are under control matters. Several tools can enforce dependencies restrictions, such as JDepend. However in a real project with many classes, packages and modules, the real issue is how to decide and configure the allowed and forbidden dependencies. Per class? Per package? Per Module? Based on gut feeling? Is there a theory for that?
Of course, in a layered architecture, the layers specify the dependencies. This is not bad, but I am sure we can do better.
To go further, I suggest expanding our vocabulary of concepts. In OO languages such as Java, everything is a class (or an interface), grouped into packages. Such classification is not really helpful. Fortunately, several books provide ready to use vocabularies in the form of patterns languages (not only design patterns, but patterns in general). Some of these patterns are foundations on which rules to manage dependencies can be proposed.
Disclaimer: the dependencies rules suggested below are hypothesises to be debated and verified against a corpus of actual projects, I would be happy to be given counter-examples and counter arguments.
Domain Driven Design
The book Domain Driven Design by Eric Evans defines a rich vocabulary of concepts used in every application, and we can leverage that vocabulary to propose some dependencies principles between them:
- ValueObject never depends upon Entity nor Services
- Entities should not depend upon Services (maybe not a hard rule)
- Generic SubDomain should not depend upon Core Domain
- Core Domain should not depend upon Cohesive Mechanism (the “What” should not depend upon the “How”)
- Domain Layer should not depend on any infrastructure code
- Abstract Core module never depends on its specialized or implementation modules
The book Analysis Patterns by Martin Fowler also provides patterns as a richer vocabulary, from which we could propose:
- Elements from a Knowledge Level should not depend upon elements from the corresponding Operation Level
I did not find that rule written in the book but every example appears to support it. Considering that classes and subclasses in usual OOP are a special case of Knowledge Level built-into the language, this would lead to:
- Abstraction never depends upon their Implementations
which is similar to the second part of the Dependency inversion principle by Robert C. Martin:
Abstractions should not depend upon details. Details should depend upon abstractions.
Since many analysis patterns in the Analysis Patterns book involve the Knowledge Level pattern, this single dependency rule already applies to many analysis patterns: Party Type Generalizations, Measurement, Observation, Protocol, Associated Observation, Measurement Protocol etc. The pattern Quantity can be seen as a specialized ValueObject (see Domain Driven Design above) hence should also not depend on any Entity nor Service.
The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. presents the classic design patterns. These patterns define participants which are named. In the pattern participant ignorance principle I discussed the concepts of ignorant Vs. dedicated participants within a pattern, and their consequences for dependencies:
- Ignorant pattern participants should never depend on dedicated participants
- Pattern participant never depend on the “Client” participant
- For each ConcreteX participant, the corresponding abstract X never depends on it (Abstraction never depends upon their Implementations)
In practice, this means:
- In the Adapter pattern, the Adaptee should not depend upon the Adapter, and the Target should depend upon nothing
- In the Facade pattern, the sub systems should not depend upon the Facade
- In the Iterator pattern, the Aggregate should not depend upon the Iterator; however every Java collection is a counter example as they contain their own ConcreteIterator.
- In creational patterns (Abstract Factory, Prototype, Builder etc.), the Product and ConcreteProduct should not depend on the dedicated participant that does the allocation (the Factory etc.)
- And so on for other patterns, some of which being already discussed in the pattern participant ignorance principle.
In short, if we look at the design patterns as a set of types with inheritance/implementation, invocation/delegation and creation relationships between them, the dependencies should not flow in the reverse direction of the relationships; in other words, using UML arrows, the dependencies should only be allowed in the direction of the arrows.
Patterns of Enterprise Architecture
In the book Patterns of Enterprise Application Architecture by Martin Fowler, the Separated Interface Pattern proposes a way to manage dependencies by defining the interface between packages in a separate package from its implementation. This is similar to the Dependency inversion principle, as discussed here, which states:
A. High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
By the way this is also very similar to the recommendation in Domain Driven Design:
Abstract Core module never depends on its specialized or implementation modules.
Finally, in the spirit of UML stereotypes that we sometimes put on packages to express their intent:
- Utils never depends on anything but other Utils
If we manage to make every use of the above pattern explicit in the source code, for instance using Java annotations or simply Javadoc tags, then it becomes possible for a tool to deduce dependencies constraints and automatically enforce them.
Imagine, just add @pattern ValueObject in your Javadoc comment, and voila! A tool is now able to deduce that if you happen to import anything but basic java.* you must be warned.
Of course the fine tuning of the default behavior can take some time (do we accept that ValueObjects may depend upon low level utils like StringUtils? Probably yes), but the result will at least be stable regardless of the refactorings.
Given the existing variety of patterns over there, I am confident that just any class or interface within a project can be declared as being a participant in at least one pattern, and have therefore its dependency constraints deduced at the same time.