javaunit-testingjunitarchunit

Enforcing Architectural Rule for BigDecimal Instantiation in Java with ArchUnit


I'm working on a Java project where we have a specific requirement: all BigDecimal instances should be created exclusively through a utility class, let's call it MathUtils. The goal is to ensure consistent application of a MathContext and other configurations.

To enforce this rule across our codebase, I am looking into using ArchUnit. However, I'm facing challenges in formulating a rule that ensures BigDecimal is only instantiated within MathUtils and not directly in other parts of the application.

Here's what I've tried so far:

  @ArchTest
  public static final ArchRule bigDecimalShouldOnlyBeInstantiatedInMathUtils = noClasses()
      .that().doNotHaveFullyQualifiedName("a.b.c.MathUtils")
      .should().callConstructor(BigDecimal.class)
      .because("BigDecimal should only be instantiated in MathUtils");

I am looking for guidance on how to properly set up an ArchUnit rule for this purpose, or if there's another approach or tool I should consider for enforcing such architectural constraints in Java.

Key Points:

  1. How to write an ArchUnit rule to ensure BigDecimal instances are only created in a specific class (MathUtils)?
  2. Are there alternative strategies or tools in Java for enforcing this kind of architectural rule?

Any insights or suggestions would be greatly appreciated!


Solution

  • In my opinion, it is a good idea to use ArchUnit to check wanted / unwanted dependencies between Java classes. Another alternative would be jQAssistant.

    For your specific requirement, 2 more things need to be considered:

    1. callConstructor(BigDecimal.class) only checks for calls to the no-args constructor, because the method is actually defined as callConstructor(Class<?> owner, Class<?>... parameterTypes)
    2. Instances of BigDecimal can also be created using the valueOf methods, where I assume that these calls are also not wanted.

    Considering these 2 aspects, you could write this rule:

    import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target;
    import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner;
    import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo;
    import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
    import static com.tngtech.archunit.lang.conditions.ArchPredicates.has;
    import static com.tngtech.archunit.lang.conditions.ArchPredicates.is;
    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
    
    @ArchTest
    public static final ArchRule bigDecimalShouldOnlyBeInstantiatedInMathUtils = noClasses()
        .that().doNotHaveFullyQualifiedName("a.b.c.MathUtils")
        .should().callConstructorWhere(targetOwner(is(equivalentTo(BigDecimal.class))))
        .orShould().callMethodWhere(targetOwner(is(equivalentTo(BigDecimal.class))).and(target(has(name("valueOf")))))
        .as("BigDecimal should only be instantiated in MathUtils");
    

    Note that I changed .because(..) to .as(..) to create a shorter rule description for error messages, because the original one may be too verbose.