Formalising code architecture using ArchUnit
Formalising code architecture using ArchUnit
I’ve been thinking about all the implicit rules in our code base recently, we ‘know’
that our endpoints should not talk directly to the data access layer and services should
not call the web layer. We also have conventions around naming things, methods that
return collections should be named findXYZ
etc.
These rules are often captured in a style guide and diligent developers will read them and incorporate them into their code. In Waltz we wanted to see if there was a way to enforce these rules in a more concrete fashion. We happened upon ArchUnit, an excellent library for describing these concerns and asserting compliance at build time via unit tests. From their website:
ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cycl ic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure. ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework
Getting Started
The tutorial is great at getting you up and running quickly, simply add a maven dependency and start writing tests. We started with some easy rules such as: “All *Dao classes should be annotated with the Repository annotation”. This can be described in a simple test:
private JavaClasses myClasses = new ClassFileImporter()
.importPackages("org.finos");
@Test
public void daosNeedRepositoryAnnotation() {
ArchRule rule = classes().that()
.areNotInterfaces()
.and()
.haveNameMatching(".*Dao")
.should()
.beAnnotatedWith(Repository.class);
rule.check(myClasses);
}
This quick success spurred us on to look at what else we could do with this library. We rapidly created tests to:
- Ensure our services and endpoints are annotated with
Service
- Check that marker interfaces and base classes are used consistently
- No calls made to
java.util.logging
(we use logback) - No generic exceptions are thrown (no
throw new Exception()
orthrow new RuntimeException()
)
The last two are provided by the library and simply need calling:
@Test
public void noGenericExceptions() {
GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS
.check(myClasses);
}
Enforcing a Layered Architecture
ArchUnit also has support for defining layers in our architecture. We simply
define layers as matching package patterns. We defined layers for our
endpoints
, data-extractors
, services
and data-access-objects
. Using these
defined layers we can write statements such as ‘api-endpoints cannot access the
data-access-objects directly, however the extractor-endpoints can’. The code ended
up looking like:
@Test
public void ensureLayersAreRespected() {
layeredArchitecture()
...
.layer("ApiEndpoints")
.definedBy("..endpoints.api..")
.layer("ExtractorEndpoints")
.definedBy("..endpoints.extracts..")
.layer("Data")
.definedBy("..data..")
...
.whereLayer("Data")
.mayOnlyBeAccessedByLayers("Services", "ExtractorEndpoints")
.check(myClasses);
}
A lot of these layers can be achieved by setting up maven sub-project
dependencies, however it’s good to have additional checks and we currently
don’t have our extractors
separated from our api-endpoints
— this lets us
put in an architectural rule to allow us to treat them as a distinct
layer within a sub-project.
Naming Conventions
Another convention within our code-base that we wished to enforce was our
naming convention within the data-access-objects. In particular we wanted
to ensure that findXYZ()
methods returned either a collection or map.
This one took a little longer to figure out and a careful reading of the
documentation. We eventually came up with a rule which looks like:
ArchCondition<JavaClass> findMethodsMustReturnCollectionsOrMaps =
new ArchCondition<JavaClass>("..desc..") {
Set<Class<?>> validTypes = SetUtilities
.asSet(Collection.class, Map.class);
public void check(JavaClass item, ConditionEvents events) {
item
.getMethods()
.stream()
.filter(m -> m.getName().startsWith("find"))
.filter(m -> m.getModifiers().contains(JavaModifier.PUBLIC))
.forEach(m -> {
JavaClass rt = m.getReturnType();
if (! any(validTypes, vrt -> rt.isAssignableTo(vrt))) {
String message = String.format(
"Method %s.%s does not return a collection or map. It returns: %s",
item.getName(),
m.getName(),
returnType.getName());
events.add(SimpleConditionEvent.violated(item, message));
}
});
}
}
private JavaClasses myClassesAndJavaUtilClasses =
new ClassFileImporter()
.importPackages("org.finos", "java.util");
@Test
public void methodsPrefixedFindShouldReturnCollections() {
ArchRule rule = classes().that()
.areNotInterfaces()
.and()
.haveNameMatching(".*Dao")
.should(haveFindMethodsThatReturnCollectionsOrMaps);
rule.check(myClassesAndJavaUtilClasses);
}
One thing that tripped us up was our original definition of myClasses
which
was simply an including classes within org.finos
, when we wished to check
the return types against java.util.Collection
and java.util.Map
the isAssignableTo
test was always failing. We fixed this by ensuring the set of classes included java.util
Conclusion
We are really pleased with how this task turned out and highly recommend that you investigate how ArchUnit could help on your projects. Many thanks to the team at TNG for making such a great library available via opensource.