Define and verify allowed dependencies between components that are represented by package structures.

Note
This tutorial is written for version 1.4.0 of jQAssistant.

1. Overview

The desired architecture of a Java project is usually described using terms like Component, Module or Layer and allowed dependencies between them. During implementation such elements must be mapped to code structures.

One approach for this is using packages, e.g. packages at a defined level are assigned the role of a Component. Furthermore each of such component might require a defined internal structure like providing a dedicated API which again is represented by a package.

For the tutorial the following package layout is used:

your.project        (1)

your.project.a      (2)
your.project.a.api  (3)
your.project.a.impl

your.project.b
your.project.b.api
your.project.b.impl

your.project.c
your.project.c.api
your.project.c.impl
  1. The root package of the project is your.project, i.e. all packages and Java types are located within this package.

  2. A package located directly within the root package represents a Component. The project consists of three components a, b and c with the following defined dependencies: ba and ca. Any other dependency is not allowed as well as circular dependencies between components, e.g. aba.

  3. Each component is structured into an api and impl package. Only types in the api packages may be used as dependency by other components to hide implementation details.

The described structural rules shall be defined and verified by jQAssistant.

2. Integrate jQAssistant Into The Build Process

The tutorial uses Apache Maven for building the project.

jQAssistant is enabled by adding the plugin to the build/plugins section of the file pom.xml.

pom.xml
<properties>
  <jqassistant.version>1.4.0</jqassistant.version>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>com.buschmais.jqassistant</groupId>
      <artifactId>jqassistant-maven-plugin</artifactId>
      <version>${jqassistant.version}</version>
      <executions>
        <execution>
          <id>default</id>
          <goals>
            <goal>scan</goal>
            <goal>analyze</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
Tip
To verify the setup a build should be triggered at this point using mvn verify. The output should contain scan and analysis messages by jQAssistant.
Note
Beside the plugin and execution declarations no further configuration is required for this tutorial.

The rules will be written as Asciidoc documents which must be located in the folder jqassistant/.

For this tutorial the following files are created:

jqassistant/index.adoc      (1)
jqassistant/maven.adoc      (2)
jqassistant/structure.adoc  (3)
  1. index.adoc is the root document, includes both other documents and defines the group default which is executed by the jQAssistant Maven plugin.

  2. maven.adoc contains a concept for identifying the main artifact which is compiled from src/main/java.

  3. structure.adoc contains the concepts and constraints for verifying the described package structure .

3. Identify The Main Artifact

Both the production code from src/main/java and test code from src/test/java are scanned by the jQAssistant Maven plugin but the described rules only apply to the production code.

Therefore a concept with the id maven:MainArtifact is defined in the file maven.adoc that adds the label Main to the required artifact:

== Maven

[[maven:MainArtifact]]
[source,cypher,role=concept]
.All JAR and WAR artifacts are labeled with `Main`.
----
MATCH
  (:Maven:Project)-[:CREATES]->(main:Artifact)
WHERE
  main.type in [
    "jar",
    "war"
  ]
SET
  main:Main  // Set the label "Main"
RETURN
  main as MainArtifact
----
Note
Test code is contained in artifacts of type test-jar.
Tip
For setting up and verifying rules it is convenient to run a build with the jQAssistant Maven plugin (see above) and start the integrated server using mvn jqassistant:server. The Neo4j browser will then be available under http://localhost:7474 for executing the queries interactively.

4. Identify Components And Define Dependencies

Using the concept maven:MainArtifact it is possible to setup the rules that describe and verify the desired package structure. These rules are located in the file structure.adoc.

First a concept is defined that identifies the root package of the application and adds a label Component to all packages which are contained in it. The list of components is returned by the rule:

[[structure:Component]]
[source,cypher,role=concept,requiresConcepts="maven:MainArtifact"]
.All packages in the root package of the main artifact are labeled as `Component`.
----
MATCH
  (:Main:Artifact)-[:CONTAINS]->(root:Package)-[:CONTAINS]->(component:Package)
WHERE
  root.fqn = "your.project"
SET
  component:Component
RETURN
  component as Component
ORDER BY
  component.name desc
----
Note
The concept depends on the result of maven:MainArtifact which therefore is referenced within requiresConcepts. jQAssistant will take care of the right execution order.

Based on the identified components allowed dependencies can be defined by adding relationships of type DEFINES_COMPONENT_DEPENDENCY. This is implemented by the concept structure:DefinedComponentDependencies:

[[structure:DefinedComponentDependencies]]
[source,cypher,role=concept,requiresConcepts="structure:Component",verify=aggregation]
.Allowed dependencies between components are represented by relationships of type `DEFINES_COMPONENT_DEPENDENCY`.
----
MATCH
  (a:Component{name:"a"}),
  (b:Component{name:"b"}),
  (c:Component{name:"c"})
MERGE
  (b)-[:DEFINES_COMPONENT_DEPENDENCY]->(a)
MERGE
  (c)-[:DEFINES_COMPONENT_DEPENDENCY]->(b)
MERGE
  (c)-[:DEFINES_COMPONENT_DEPENDENCY]->(a)
RETURN
  count(*) as Count
----
Note
The concept returns an aggregated result, i.e. exactly one row created by count(*). For correct verification by jQAssistant verify=aggregation needs to be specified. This strategy checks the value of the first column (i.e. Count) to be greater than 0. If omitted the default behavior would apply and verify that at least one row is returned.

5. Identify Existing Dependencies

The concept structure:ComponentDependencies determines the actual dependencies on type level and aggregates them to component level:

[[structure:ComponentDependencies]]
[source,cypher,role=concept,requiresConcepts="structure:Component"]
.A component depends on another component if there is a dependency on type level between both. These dependencies are represented by relationships of type `DEPENDS_ON_COMPONENT`.
----
MATCH
  (c1:Component)-[:CONTAINS*]->(t1:Type),
  (c2:Component)-[:CONTAINS*]->(t2:Type),
  (t1)-[:DEPENDS_ON]->(t2)
WHERE
  c1 <> c2
WITH
  c1, c2, count(*) as weight
MERGE
  (c1)-[d:DEPENDS_ON_COMPONENT]->(c2)
SET
  d.weight = weight
RETURN
  c1 as Component, c2 as ComponentDependency, d as Dependency
----
Note
The asterisk in the CONTAINS patterns of the MATCH clause is used to collect all types within a component through all contained package levels.
Tip
The DEPENDS_ON_COMPONENT relationships are enriched by a property weight which is the count of all type dependencies. This value can be used as an indicator for the degree of coupling between two components.

6. Verify Existing Against Defined Dependencies

At this point all required information is available for setting up a constraint structure:UndefinedComponentDependencies. This rule verifies that for each DEPENDS_ON_COMPONENT relationship between two components also a relationship DEFINES_COMPONENT_DEPENDENCY exists. If it is not present then the types creating this unwanted dependency are identified and returned:

[[structure:UndefinedComponentDependencies]]
[source,cypher,role=constraint,requiresConcepts="structure:DefinedComponentDependencies,structure:ComponentDependencies"]
.Dependencies between components are only allowed if there is a defined dependency between them.
----
MATCH
  (c1:Component)-[:DEPENDS_ON_COMPONENT]->(c2:Component)
WHERE NOT
  (c1:Component)-[:DEFINES_COMPONENT_DEPENDENCY]->(c2:Component)
WITH
  c1, c2
MATCH
  (c1)-[:CONTAINS*]->(t1:Type),
  (c2)-[:CONTAINS*]->(t2:Type),
  (t1)-[:DEPENDS_ON]->(t2)
RETURN
  c1 as Component, t1 as Type, c2 as InvalidComponentDependency, collect(t2) as InvalidTypeDependencies
----

A similar constraint may used to verify if a dependency between components is defined but no dependency exists in the code. This situation is less critical but it is recommendable to remove such obsolete definitions:

[[structure:UnusedComponentDependencies]]
[source,cypher,role=constraint,requiresConcepts="structure:DefinedComponentDependencies,structure:ComponentDependencies"]
.There must be not defined but unused dependencies between components.
----
MATCH
  (c1:Component)-[:DEFINES_COMPONENT_DEPENDENCY]->(c2:Component)
WHERE NOT
  (c1:Component)-[:DEPENDS_ON_COMPONENT]->(c2:Component)
RETURN
  c1 as Component, c2 as UnusedDependency
----

7. Prevent Cyclic Dependencies

A cyclic dependency may occur if a component indirectly references itself as a dependency, e.g. aba. Such cycles should be considered as a serious problem because they decrease comprehensibility of the application and make refactorings or restructurings complicated.

Using the rules explained above unwanted cycles are prevented under the condition that the defined dependencies are cycle-free. But in larger projects consisting of many components it may happen that this problem is introduced by accident into the definition itself. For this reason an additional constraint structure:CyclicComponentDependencies helps detecting such situation early:

[[structure:CyclicComponentDependencies]]
[source,cypher,role=constraint,requiresConcepts="structure:ComponentDependencies"]
.There must be no cyclic dependencies between components.
----
MATCH
  (c1:Component)-[:DEFINES_COMPONENT_DEPENDENCY]->(c2:Component),
  cycle=shortestPath((c2)-[:DEFINES_COMPONENT_DEPENDENCY*]->(c1))
RETURN
  c1 as Component, nodes(cycle) as Cycle
----

8. API And Implementation

The packages containing APIs and implementations of all components can be identified by the concepts structure:Api and structure:Impl:

[[structure:Api]]
[source,cypher,role=concept,requiresConcepts="structure:Component"]
.API classes of a component are located in the package `api` directly located within the component. This package is labeled with `Api`.
----
MATCH
  (component:Component)-[:CONTAINS]->(api:Package)
WHERE
  api.name = "api"
SET
  api:Api
RETURN
  component as Component, api as Api
----
[[structure:Impl]]
[source,cypher,role=concept,requiresConcepts="structure:Component"]
.Implementation classes of a component are located in the package `impl` located within the component. This package is labeled with `Impl`.
----
MATCH
  (component:Component)-[:CONTAINS]->(impl:Package)
WHERE
  impl.name = "impl"
SET
  impl:Impl
RETURN
  component as Component, impl as Impl
----

The concepts add labels Api and Impl which can now be used to define constraints. To prevent unintentional leaking of implementation details the constraint structure:ApiMustNotDependOnImpl verifies that a component’s API does not depend on its implementation:

[[structure:ApiMustNotDependOnImpl]]
[source,cypher,role=constraint,requiresConcepts="structure:Api,structure:Impl"]
.API types must not depend on implementation types of a component.
----
MATCH
  (component:Component),
  (component)-[:CONTAINS]->(:Api)-[:CONTAINS*]->(api:Type),
  (component)-[:CONTAINS]->(:Impl)-[:CONTAINS*]->(impl:Type),
  (api)-[:DEPENDS_ON]->(impl)
RETURN
  component as Component, api as ApiType, impl as ImplType
----

The following constraint structure:ComponentDependencyMustUseApi ensures that only API types of components are used to create dependencies:

[[structure:ComponentDependencyMustUseApi]]
[source,cypher,role=constraint,requiresConcepts="structure:Api,structure:Impl,structure:ComponentDependencies"]
.Only types provided by a component API may be used as dependency by another component.
----
MATCH
  (c1:Component)-[:DEPENDS_ON_COMPONENT]->(c2:Component),
  (c1)-[:CONTAINS*]->(t1:Type),
  (c2)-[:CONTAINS*]->(t2:Type),
  (t1)-[:DEPENDS_ON]->(t2)
WHERE NOT
  (:Api)-[:CONTAINS*]->(t2)
RETURN
  c1 as Component, t1 as Type, c2 as ComponentDependency, t2 as InvalidTypeDependency
----

9. Activate The Rules

For execution the constraints should be included within a group. The file jqassistant/structure.adoc therefore defines structure:Default:

[[structure:Default]]
[role=group,includesConstraints="structure:UndefinedComponentDependencies,structure:UnusedComponentDependencies,structure:CyclicComponentDependencies,structure:ApiMustNotDependOnImpl,structure:ComponentDependencyMustUseApi"]
== Structure

This document describes the rules regarding package dependencies.

This group itself is included within the group default that is defined in jqassistant/index.adoc and executed by default:

[[default]]
[role=group,includesGroups="structure:Default"]
== Default Rules

This section describes that default rules that are executed during each build.

- <<structure:Default>>
Tip
The shown approach of building up a hierarchy between groups is the recommended way of organizing rules. It’s also possible to reference rules to be executed directly within the jQAssistant Maven plugin configuration. For details refer to the manual.

10. Resources