Define and verify allowed dependencies between components that are represented by package structures.
Note
|
This tutorial is written for version 1.9.1 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
-
The root package of the project is
your.project
, i.e. all packages and Java types are located within this package. -
A package located directly within the root package represents a
Component
. The project consists of three componentsa
,b
andc
with the following defined dependencies:b
→a
andc
→a
. Any other dependency is not allowed as well as circular dependencies between components, e.g.a
→b
→a
. -
Each component is structured into an
api
andimpl
package. Only types in theapi
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
.
<properties>
<jqassistant.version>1.9.1</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/structure.adoc (2)
-
index.adoc is the root document, includes both other documents and defines the group
default
which is executed by the jQAssistant Maven plugin. -
structure.adoc contains the concepts and constraints for verifying the described package structure .
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.
|
3. Identify Components And Define Dependencies
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]
.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
----
Both production code from src/main/java
and test code from src/test/java
are scanned by the jQAssistant Maven plugin.
The described rules only apply to production code which is contained in an artifact labeled with Main
.
Note
|
Test code is identified by the label Test .
|
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.
|
4. 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.
|
5. 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
----
6. Prevent Cyclic Dependencies
A cyclic dependency may occur if a component indirectly references itself as a dependency, e.g. a
→ b
→ a
.
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
----
7. 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
----
8. 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. |
9. Resources
-
Managing Technical Debt with arc42 and jQAssistant: Building Block Dependencies (Jens Nerche, Kontext E GmbH)
-
Preventing leaky APIs with jQAssistant (Gunnar Morling)