Vipin
Vipin

Reputation: 5223

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!

Upvotes: 0

Views: 103

Answers (2)

Manfred
Manfred

Reputation: 3142

You're almost there! Note, however, that callConstructor(BigDecimal.class) would only match a BigDecimal constructor without parameters, which is probably not what you want (and which doesn't even exist in my JDK).

If you want to forbid any BigDecimal constructor call, you could use the following rule:

@ArchTest
ArchRule bigDecimalShouldOnlyBeInstantiatedInMathUtils = noClasses()
    .that().doNotHaveFullyQualifiedName(MathUtils.class.getName())
    .should().callConstructorWhere(describe("target is BigDecimal", constructorCall ->
            constructorCall.getTargetOwner().isEquivalentTo(BigDecimal.class)
    ))
    .because("BigDecimal should only be instantiated in MathUtils");

Upvotes: 1

Roland Weisleder
Roland Weisleder

Reputation: 10531

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.

Upvotes: 1

Related Questions