Skip to content

Commit

Permalink
Allow defining Onion Architecture components by predicates #894
Browse files Browse the repository at this point in the history
Similar to `LayeredArchitecture` this will extend `OnionArchitecture` to not only allow defining components via package identifiers (e.g. `domainModels("..some.pkg..")`) but also via predicates (e.g. `domainModels(annotatedWith(DomainModel.class))` or `domainModels(simpleNameEndingWith("Model"))`).
This will allow users that follow a different convention than packages to identify their Onion Architecture components to also use `OnionArchitecture` to assert their architecture.
  • Loading branch information
codecholeric authored Jun 30, 2022
2 parents e779a7a + 6046075 commit 5b1b7c3
Show file tree
Hide file tree
Showing 53 changed files with 902 additions and 146 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.tngtech.archunit.exampletest.junit4;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.PackageMatchers;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMember;
import com.tngtech.archunit.core.domain.PackageMatchers;
import com.tngtech.archunit.example.layers.security.Secured;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
package com.tngtech.archunit.exampletest.junit4;

import java.lang.annotation.Annotation;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem;
import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;

@Category(Example.class)
@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture")
@AnalyzeClasses(packages = {
"com.tngtech.archunit.example.onionarchitecture",
"com.tngtech.archunit.example.onionarchitecture_by_annotations"
})
public class OnionArchitectureTest {

@ArchTest
Expand All @@ -35,4 +51,30 @@ public class OnionArchitectureTest {
.adapter("rest", "..adapter.rest..")

.ignoreDependency(OrderItem.class, OrderQuantity.class);

@ArchTest
static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture()
.domainModels(byAnnotation(DomainModel.class))
.domainServices(byAnnotation(DomainService.class))
.applicationServices(byAnnotation(Application.class))
.adapter("cli", byAnnotation(adapter("cli")))
.adapter("persistence", byAnnotation(adapter("persistence")))
.adapter("rest", byAnnotation(adapter("rest")));

private static DescribedPredicate<JavaClass> byAnnotation(Class<? extends Annotation> annotationType) {
DescribedPredicate<CanBeAnnotated> annotatedWith = annotatedWith(annotationType);
return belongTo(annotatedWith).as(annotatedWith.getDescription());
}

private static DescribedPredicate<JavaClass> byAnnotation(DescribedPredicate<? super JavaAnnotation<?>> annotationType) {
DescribedPredicate<CanBeAnnotated> annotatedWith = annotatedWith(annotationType);
return belongTo(annotatedWith).as(annotatedWith.getDescription());
}

private static DescribedPredicate<JavaAnnotation<?>> adapter(String adapterName) {
return describe(
String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName),
a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.net.URL;

import com.tngtech.archunit.base.PackageMatchers;
import com.tngtech.archunit.core.domain.PackageMatchers;
import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog;
import com.tngtech.archunit.example.plantuml.order.Order;
import com.tngtech.archunit.example.plantuml.product.Product;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.tngtech.archunit.exampletest.junit5;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.PackageMatchers;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMember;
import com.tngtech.archunit.core.domain.PackageMatchers;
import com.tngtech.archunit.example.layers.security.Secured;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTag;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
package com.tngtech.archunit.exampletest.junit5;

import java.lang.annotation.Annotation;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem;
import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTag;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;

@ArchTag("example")
@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture")
@AnalyzeClasses(packages = {
"com.tngtech.archunit.example.onionarchitecture",
"com.tngtech.archunit.example.onionarchitecture_by_annotations"
})
public class OnionArchitectureTest {

@ArchTest
Expand All @@ -32,4 +48,30 @@ public class OnionArchitectureTest {
.adapter("rest", "..adapter.rest..")

.ignoreDependency(OrderItem.class, OrderQuantity.class);

@ArchTest
static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture()
.domainModels(byAnnotation(DomainModel.class))
.domainServices(byAnnotation(DomainService.class))
.applicationServices(byAnnotation(Application.class))
.adapter("cli", byAnnotation(adapter("cli")))
.adapter("persistence", byAnnotation(adapter("persistence")))
.adapter("rest", byAnnotation(adapter("rest")));

private static DescribedPredicate<JavaClass> byAnnotation(Class<? extends Annotation> annotationType) {
DescribedPredicate<CanBeAnnotated> annotatedWith = annotatedWith(annotationType);
return belongTo(annotatedWith).as(annotatedWith.getDescription());
}

private static DescribedPredicate<JavaClass> byAnnotation(DescribedPredicate<? super JavaAnnotation<?>> annotationType) {
DescribedPredicate<CanBeAnnotated> annotatedWith = annotatedWith(annotationType);
return belongTo(annotatedWith).as(annotatedWith.getDescription());
}

private static DescribedPredicate<JavaAnnotation<?>> adapter(String adapterName) {
return describe(
String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName),
a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.net.URL;

import com.tngtech.archunit.base.PackageMatchers;
import com.tngtech.archunit.core.domain.PackageMatchers;
import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog;
import com.tngtech.archunit.example.plantuml.order.Order;
import com.tngtech.archunit.example.plantuml.product.Product;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations;

public @interface Adapter {
String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations;

public @interface Application {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations;

public @interface DomainModel {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations;

public @interface DomainService {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application;

@Application
public interface AdministrationPort {
<T> T getInstanceOf(Class<T> type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application;

@Application
public class ShoppingApplication {
public static void main(String[] args) {
// start the whole application / provide IOC features
}

public static AdministrationPort openAdministrationPort() {
return new AdministrationPort() {
@Override
public <T> T getInstanceOf(Class<T> type) {
throw new UnsupportedOperationException("Not yet implemented");
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.administration;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.AdministrationPort;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.ShoppingApplication;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository;

@Adapter("cli")
@SuppressWarnings("unused")
public class AdministrationCLI {
public static void main(String[] args) {
AdministrationPort port = ShoppingApplication.openAdministrationPort();
handle(args, port);
}

private static void handle(String[] args, AdministrationPort port) {
// violates the pairwise independence of adapters
ProductRepository repository = port.getInstanceOf(ProductRepository.class);
long count = repository.getTotalCount();
// parse arguments and re-configure application according to count through port
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel;
import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product;

@DomainModel
public class OrderItem {
private final Product product;
private final OrderQuantity quantity;

public OrderItem(Product product, OrderQuantity quantity) {
if (product == null) {
throw new IllegalArgumentException("Product must not be null");
}
if (quantity == null) {
throw new IllegalArgumentException("Quantity not be null");
}
this.product = product;
this.quantity = quantity;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{product=" + product + ", quantity=" + quantity + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService;

@DomainService
@SuppressWarnings("unused")
public class OrderQuantity {
private final int quantity;

public OrderQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
this.quantity = quantity;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{quantity=" + quantity + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel;

@DomainModel
public class PaymentMethod {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel;

@DomainModel
public class Product {
// Dependency on ProductId violates the architecture, since ProductId resides with persistence adapter
private final ProductId id;
// Dependency on ProductName violates the architecture, since ProductName is located in the DomainService layer
private final ProductName name;

public Product(ProductId id, ProductName name) {
if (id == null) {
throw new IllegalArgumentException("Product id must not be null");
}
if (name == null) {
throw new IllegalArgumentException("Product name must not be null");
}
this.id = id;
this.name = name;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{id=" + id + ", name=" + name + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product;

import java.util.UUID;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter;

@Adapter("persistence")
@SuppressWarnings("unused")
public class ProductId {
private final UUID id;

public ProductId(UUID id) {
if (id == null) {
throw new IllegalArgumentException("ID must not be null");
}
this.id = id;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{id=" + id + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter;

@Adapter("persistence")
@SuppressWarnings("unused")
public class ProductJpaRepository implements ProductRepository {
@Override
public Product read(ProductId id) {
return new Product(id, new ProductName("would normally be read"));
}

@Override
public long getTotalCount() {
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product;

import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService;

@DomainService
@SuppressWarnings("unused")
public class ProductName {
private final String name;

public ProductName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must not be empty");
}
this.name = name;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{name='" + name + '\'' + '}';
}
}
Loading

0 comments on commit 5b1b7c3

Please sign in to comment.