From b581e6db167da99ff68dbcb71e0d588960564f9b Mon Sep 17 00:00:00 2001 From: Tom Hombergs Date: Sun, 16 Jun 2019 07:57:40 +0200 Subject: [PATCH] added hexagonal ArchUnit check --- .../reviewapp/DependencyRuleTests.java | 44 ++++++++++++ .../reviewapp/archunit/Adapters.java | 62 +++++++++++++++++ .../reviewapp/archunit/ApplicationLayer.java | 59 ++++++++++++++++ .../archunit/ArchitectureElement.java | 69 +++++++++++++++++++ .../archunit/HexagonalArchitecture.java | 61 ++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 configuration/src/test/java/io/reflectoring/reviewapp/DependencyRuleTests.java create mode 100644 configuration/src/test/java/io/reflectoring/reviewapp/archunit/Adapters.java create mode 100644 configuration/src/test/java/io/reflectoring/reviewapp/archunit/ApplicationLayer.java create mode 100644 configuration/src/test/java/io/reflectoring/reviewapp/archunit/ArchitectureElement.java create mode 100644 configuration/src/test/java/io/reflectoring/reviewapp/archunit/HexagonalArchitecture.java diff --git a/configuration/src/test/java/io/reflectoring/reviewapp/DependencyRuleTests.java b/configuration/src/test/java/io/reflectoring/reviewapp/DependencyRuleTests.java new file mode 100644 index 0000000..88c8569 --- /dev/null +++ b/configuration/src/test/java/io/reflectoring/reviewapp/DependencyRuleTests.java @@ -0,0 +1,44 @@ +package io.reflectoring.reviewapp; + +import com.tngtech.archunit.core.importer.ClassFileImporter; +import io.reflectoring.reviewapp.archunit.HexagonalArchitecture; +import org.junit.jupiter.api.Test; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; + +class DependencyRuleTests { + + @Test + void validateRegistrationContextArchitecture() { + HexagonalArchitecture.boundedContext("io.reflectoring.reviewapp") + + .withDomainLayer("domain") + + .withAdaptersLayer("adapter") + .incoming("web") + .outgoing("persistence") + .and() + + .withApplicationLayer("application") + .services("service") + .incomingPorts("port.in") + .outgoingPorts("port.out") + .and() + + .withConfiguration("configuration") + .check(new ClassFileImporter() + .importPackages("io.reflectoring.reviewapp..")); + } + + @Test + void testPackageDependencies() { + noClasses() + .that() + .resideInAPackage("io.reflectoring.reviewapp.domain..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("io.reflectoring.reviewapp.application..") + .check(new ClassFileImporter() + .importPackages("io.reflectoring.reviewapp..")); + } + +} diff --git a/configuration/src/test/java/io/reflectoring/reviewapp/archunit/Adapters.java b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/Adapters.java new file mode 100644 index 0000000..73a487c --- /dev/null +++ b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/Adapters.java @@ -0,0 +1,62 @@ +package io.reflectoring.reviewapp.archunit; + +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.archunit.core.domain.JavaClasses; + +public class Adapters extends ArchitectureElement { + + private final HexagonalArchitecture parentContext; + private List incomingAdapterPackages = new ArrayList<>(); + private List outgoingAdapterPackages = new ArrayList<>(); + + Adapters(HexagonalArchitecture parentContext, String basePackage) { + super(basePackage); + this.parentContext = parentContext; + } + + public Adapters outgoing(String packageName) { + this.incomingAdapterPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public Adapters incoming(String packageName) { + this.outgoingAdapterPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + List allAdapterPackages() { + List allAdapters = new ArrayList<>(); + allAdapters.addAll(incomingAdapterPackages); + allAdapters.addAll(outgoingAdapterPackages); + return allAdapters; + } + + public HexagonalArchitecture and() { + return parentContext; + } + + String getBasePackage() { + return basePackage; + } + + void dontDependOnEachOther(JavaClasses classes) { + List allAdapters = allAdapterPackages(); + for (String adapter1 : allAdapters) { + for (String adapter2 : allAdapters) { + if (!adapter1.equals(adapter2)) { + denyDependency(adapter1, adapter2, classes); + } + } + } + } + + void doesNotDependOn(String packageName, JavaClasses classes) { + denyDependency(this.basePackage, packageName, classes); + } + + void doesNotContainEmptyPackages() { + denyEmptyPackages(allAdapterPackages()); + } +} diff --git a/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ApplicationLayer.java b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ApplicationLayer.java new file mode 100644 index 0000000..133cb93 --- /dev/null +++ b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ApplicationLayer.java @@ -0,0 +1,59 @@ +package io.reflectoring.reviewapp.archunit; + +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.archunit.core.domain.JavaClasses; + +public class ApplicationLayer extends ArchitectureElement { + + private final HexagonalArchitecture parentContext; + private List incomingPortsPackages = new ArrayList<>(); + private List outgoingPortsPackages = new ArrayList<>(); + private List servicePackages = new ArrayList<>(); + + public ApplicationLayer(String basePackage, HexagonalArchitecture parentContext) { + super(basePackage); + this.parentContext = parentContext; + } + + public ApplicationLayer incomingPorts(String packageName) { + this.incomingPortsPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public ApplicationLayer outgoingPorts(String packageName) { + this.outgoingPortsPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public ApplicationLayer services(String packageName) { + this.servicePackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public HexagonalArchitecture and() { + return parentContext; + } + + public void doesNotDependOn(String packageName, JavaClasses classes) { + denyDependency(this.basePackage, packageName, classes); + } + + public void incomingAndOutgoingPortsDoNotDependOnEachOther(JavaClasses classes) { + denyAnyDependency(this.incomingPortsPackages, this.outgoingPortsPackages, classes); + denyAnyDependency(this.outgoingPortsPackages, this.incomingPortsPackages, classes); + } + + private List allPackages() { + List allPackages = new ArrayList<>(); + allPackages.addAll(incomingPortsPackages); + allPackages.addAll(outgoingPortsPackages); + allPackages.addAll(servicePackages); + return allPackages; + } + + void doesNotContainEmptyPackages() { + denyEmptyPackages(allPackages()); + } +} diff --git a/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ArchitectureElement.java b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ArchitectureElement.java new file mode 100644 index 0000000..12e074f --- /dev/null +++ b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/ArchitectureElement.java @@ -0,0 +1,69 @@ +package io.reflectoring.reviewapp.archunit; + +import java.util.List; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import static com.tngtech.archunit.base.DescribedPredicate.*; +import static com.tngtech.archunit.lang.conditions.ArchConditions.*; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; + +abstract class ArchitectureElement { + + final String basePackage; + + public ArchitectureElement(String basePackage) { + this.basePackage = basePackage; + } + + String fullQualifiedPackage(String relativePackage) { + return this.basePackage + "." + relativePackage; + } + + static void denyDependency(String fromPackageName, String toPackageName, JavaClasses classes) { + noClasses() + .that() + .resideInAPackage("io.reflectoring.reviewapp.domain..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("io.reflectoring.reviewapp.application..") + .check(classes); + } + + static void denyAnyDependency( + List fromPackages, List toPackages, JavaClasses classes) { + for (String fromPackage : fromPackages) { + for (String toPackage : toPackages) { + noClasses() + .that() + .resideInAPackage(matchAllClassesInPackage(fromPackage)) + .should() + .dependOnClassesThat() + .resideInAnyPackage(matchAllClassesInPackage(toPackage)) + .check(classes); + } + } + } + + static String matchAllClassesInPackage(String packageName) { + return packageName + ".."; + } + + void denyEmptyPackage(String packageName) { + classes() + .that() + .resideInAPackage(matchAllClassesInPackage(packageName)) + .should(containNumberOfElements(greaterThanOrEqualTo(1))) + .check(classesInPackage(packageName)); + } + + private JavaClasses classesInPackage(String packageName) { + return new ClassFileImporter().importPackages(packageName); + } + + void denyEmptyPackages(List packages) { + for (String packageName : packages) { + denyEmptyPackage(packageName); + } + } +} diff --git a/configuration/src/test/java/io/reflectoring/reviewapp/archunit/HexagonalArchitecture.java b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/HexagonalArchitecture.java new file mode 100644 index 0000000..3a1efe8 --- /dev/null +++ b/configuration/src/test/java/io/reflectoring/reviewapp/archunit/HexagonalArchitecture.java @@ -0,0 +1,61 @@ +package io.reflectoring.reviewapp.archunit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.tngtech.archunit.core.domain.JavaClasses; + +public class HexagonalArchitecture extends ArchitectureElement { + + private Adapters adapters; + private ApplicationLayer applicationLayer; + private String configurationPackage; + private List domainPackages = new ArrayList<>(); + + public static HexagonalArchitecture boundedContext(String basePackage) { + return new HexagonalArchitecture(basePackage); + } + + public HexagonalArchitecture(String basePackage) { + super(basePackage); + } + + public Adapters withAdaptersLayer(String adaptersPackage) { + this.adapters = new Adapters(this, fullQualifiedPackage(adaptersPackage)); + return this.adapters; + } + + public HexagonalArchitecture withDomainLayer(String domainPackage) { + this.domainPackages.add(fullQualifiedPackage(domainPackage)); + return this; + } + + public ApplicationLayer withApplicationLayer(String applicationPackage) { + this.applicationLayer = new ApplicationLayer(fullQualifiedPackage(applicationPackage), this); + return this.applicationLayer; + } + + public HexagonalArchitecture withConfiguration(String packageName) { + this.configurationPackage = fullQualifiedPackage(packageName); + return this; + } + + private void domainDoesNotDependOnOtherPackages(JavaClasses classes) { + denyAnyDependency( + this.domainPackages, Collections.singletonList(adapters.basePackage), classes); + denyAnyDependency( + this.domainPackages, Collections.singletonList(applicationLayer.basePackage), classes); + } + + public void check(JavaClasses classes) { + this.adapters.doesNotContainEmptyPackages(); + this.adapters.dontDependOnEachOther(classes); + this.adapters.doesNotDependOn(this.configurationPackage, classes); + this.applicationLayer.doesNotContainEmptyPackages(); + this.applicationLayer.doesNotDependOn(this.adapters.getBasePackage(), classes); + this.applicationLayer.doesNotDependOn(this.configurationPackage, classes); + this.applicationLayer.incomingAndOutgoingPortsDoNotDependOnEachOther(classes); + this.domainDoesNotDependOnOtherPackages(classes); + } +}