Control package dependencies with Checkstyle

You want the internal structure of the code inside a component to keep its shape during development.
The chosen development approach, like Domain Driven Design, gives you design guidelines for shaping the component, including dependency rules (E.g. domain model to not depend on infrastructure).
Bigger the project, harder is to catch package dependency violations using standard practices like code reviews. At a higher level, the dependencies between components are easier to control. For example, you can rely on Maven dependencies which means problems are caught at compile time.
But inside a component, even if you go smaller - with one microservice per bounded context, non-trivial domains will contain a considerable number of classes and packages.

The solution is to implement a quality gateway for this architectural aspect, aka defining a Fitness Function according to the book Building Evolutionary Architectures. The book contains examples with JDepend but I found it very hard to use JDepend on a real project. This is why I'm sharing a simpler alternative.

Checkstyle has an Import Control component plus Maven integration. It has a rather steep learning curve due to its flexibility but, once understood, it's a powerful tool.
You can see it in action at https://github.com/vichim/deps-control

The example application assumes a DDD-like component in which we want to preserve both the dependencies between the higher level business concepts (see the green arrow below) and the dependencies between layers (blue arrows).

The requirements above map to the following Checkstyle definitions (see the repository for the more complete definitions):
Under the "library" module you can see the "domain" package can depend only on itself. Then the "service" package can depend on itself plus "domain". Then "infrastructure" depends on all.
At a higher level there is an uni-directional dependency from "sales" to "library".

Validation can simply be done with the Maven command:
mvn checkstyle:check
Optionally, you can break the build, on mvn clean install, when Checkstyle reports errors.
The example project enables both options.

When I try to intentionally break the rules, like linking a domain object to a presentation one, I get the following error on mvn compile:
[INFO] There is 1 error reported by Checkstyle 6.18 with control/checkstyle/checkstyle.xml ruleset.
[ERROR] src\main\java\ro\vichim\depscontrol\bookstore\library\domain\Book.java:[3,1] (imports) ImportControl: Disallowed import - ro.vichim.depscontrol.bookstore.library.infrastructure.presentation.BookController.
[INFO] ---------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ---------------------------------------------------

Tips

  1. Don't go too deep with the rules. Two levels should be enough:
    • Describe dependencies between higher level business concerns.
      See "sales" module depending on "library" module.
    • Describe dependencies between layers for each sub-module.
      See "service" packages depending on "domain" ones but not inverse.
  2. Utils packages can be defined only once at the highest level.
    See <allow pkg="ro.vichim.depscontrol.bookstore.utils"/>
  3. Classes allowed to see multiple sub-modules need "local-only" rules to avoid cascading the permission.
    See the rule for BookstoreFacade
    <allow class="ro.vichim.depscontrol.bookstore..*\..*" regex="true" local-only="true" />
  4. Util classes with narrower scope may need access from multiple layers until a better place is found for them.
    See the rule for SubModuleUtil
    <allow class="ro.vichim.depscontrol.bookstore.sales.[^.]+" regex="true"/>
  5. Have the Checkstyle DTDs in the project.
    You don't want to fail the build when the Checkstyle web site is down.
  6. Use SuppressionFilter to exclude packages from analysis.
    Useful when the rules are created progressively on an existing project and it takes time to resolve illegal dependencies.
  7. Complement ImportControl with other checks.
    The example project enables UnusedImports, helpful to avoid false dependencies.

Advantages

  1. Fresh and executable documentation of the code structure
  2. Prevents degradation of high level design
  3. Encourages design consistency
    Makes you think more about coupling and cohesion.
  4. Facilitates later extraction of services
  5. Helps code reviews
    Illegal coupling is not easy to spot all the time.
  6. Impact of code changes is easier to understand given clear internal dependency graph.

The dependency rules should be maintained as any other code and requires attention to the business rules and to the type of architecture chosen.
This mechanism cannot fully control coupling (think of SQL level coupling) but it's a good start. Together with other Fitness Functions, it can be installed as an explicit step in your build pipeline.

Note that, without such a control on the high level design, you risk to be bitten later by expensive refactorings.

Comments

Popular posts from this blog

Test Doubles by Example