Monday 27 August 2018

Enforcing Multi-Tier Architecture

So you've designed an application, using the principals of separation of concerns and a multi-tier architecture. It's a delight to navigate and maintain the code base, the architecture might look something like this:

The presentation layer talks to the application layer, which talks to the data access layer. The facade object provides a high-level interface for API consumers, talking to the service objects, which call objects encapsulating business logic, which operate on data provided by the data access objects. Life is good.

Eventually other programmers will have to maintain and add new features to your application, possibly in your absence. How do you communicate your design intentions to future maintainers? The above diagram, a bit of documentation, and some programming rigour should suffice. Back in the real world, programmers face time pressures which prevent them creating and updating documentation, and managers and customers don't care about code maintainability - they want their features yesterday. When getting the code into production as fast as possible is the only focus, clean code and architecture are soon forgotten.

To quote John Carmack:

"It’s just amazing how many mistakes and how bad programmers can be. Everything that is syntactically legal, that the compiler will accept, will eventually wind up in your code base."

Carmack was talking about the usefulness of static typing here, but the same problem also applies to code architecture: over time, whatever can happen, will happen. Your well designed architecture will risk turning into spaghetti code, with objects calling methods from any layer:

To address this problem I think it would be useful to have a way of documenting and enforcing which objects can invoke a method on another object. In Java this can be achieved with a couple of annotations and some aspect oriented programming. Below is an annotation named CallableFrom which can be used to annotate methods on a class indicating what classes and interface implementations the method can be called from.

CallableFrom.java

package org.adrianwalker.callablefrom;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.adrianwalker.callablefrom.test.TestCaller;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CallableFrom {

  CallableFromClass[] value() default {
    @CallableFromClass(TestCaller.class)
  };
}

The annotation's value method returns an array of another annotation CallableFromClass:

CallableFromClass.java

package org.adrianwalker.callablefrom;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CallableFromClass {

  Class value();

  boolean subclasses() default true;
}

The annotation's value method returns a Class object - the class (or interface) of an object which is allowed to call the annotated method. The annotation's subclasses method returns a boolean value which flags if subclasses (or interface implementations) are allowed to call the annotated method.

At this point the annotations do nothing, we need a way of enforcing the behaviour specified by the annotations. This can be achieved using an AspectJ aspect class:

CallableFromAspect.java

package org.adrianwalker.callablefrom;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public final class CallableFromAspect {

  @Before("@annotation(callableFrom) && call(* *.*(..))")
  public void before(final JoinPoint joinPoint, final CallableFrom callableFrom) throws CallableFromError {

    Class callingClass = joinPoint.getThis().getClass();
    boolean isCallable = isCallable(callableFrom, callingClass);

    if (!isCallable) {
      Class targetClass = joinPoint.getTarget().getClass();
      throw new CallableFromError(targetClass, callingClass);
    }
  }

  private boolean isCallable(final CallableFrom callableFrom, final Class callingClass) {

    boolean callable = false;
    CallableFromClass[] callableFromClasses = callableFrom.value();

    for (CallableFromClass callableFromClass : callableFromClasses) {

      Class clazz = callableFromClass.value();
      boolean subclasses = callableFromClass.subclasses();

      callable = (subclasses && clazz.isAssignableFrom(callingClass))
              || (!subclasses && clazz.equals(callingClass));

      if (callable) {
        break;
      }
    }

    return callable;
  }
}

The aspect intercepts any calls to methods annotated with @CallableFrom, gets the calling object's class and compares it to the class objects specified by the @CallableFromClass's class values. If subclasses is true (the default), the calling class can be a subclass (or implementation) of the class object specified by @CallableFromClass. If subclasses is false the calling class must be equal to the class object specified by @CallableFromClass.

If the above conditions are not met, for any of the @CallableFromClass annotations, the method is not callable from the calling class and a CallableFromError error is thrown. CallableFromError extends Error rather than Exception as it is not expected that application code should ever to attempt to catch it.

CallableFromError.java

package org.adrianwalker.callablefrom;

public final class CallableFromError extends Error {

  private static final String EXCEPTION_MESSAGE = "%s is not callable from %s";

  public CallableFromError(final Class targetClass, final Class callingClass) {

    super(String.format(EXCEPTION_MESSAGE,
            targetClass.getCanonicalName(),
            callingClass.getCanonicalName()));
  }
}

For example, if you have a class named Callable and you only want to be able to call it from another class named CallableCaller, no subclasses:

Callable.java

package org.adrianwalker.callablefrom;

public final class Callable {

  @CallableFrom({
    @CallableFromClass(value=CallableCaller.class, subclasses=false)
  })
  public void doStuff() {

    System.out.println("Callable doing stuff");
  }
}

Another example, if you had some business logic encapsulated in an object which should only be called by a service object and test classes:

UpperCaseBusinessObject.java

package org.adrianwalker.callablefrom.example.application;

import org.adrianwalker.callablefrom.CallableFrom;
import org.adrianwalker.callablefrom.CallableFromClass;
import org.adrianwalker.callablefrom.test.TestCaller;

public final class UpperCaseBusinessObject implements ApplicationLayer {

  @CallableFrom({
    @CallableFromClass(value = MessageService.class, subclasses = false),
    @CallableFromClass(value = TestCaller.class, subclasses = true)
  })
  public String uppercaseMessage(final String message) {

    if (null == message) {
      return null;
    }

    return message.toUpperCase();
  }
}

Testing

To make classes callable from JUnit tests, the unit test class should implement the TestCaller interface. This interface is the default value for the CallableFrom annotation:

CallableFromTest.java

package org.adrianwalker.callablefrom;

import org.adrianwalker.callablefrom.test.TestCaller;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;

public final class CallableFromTest implements TestCaller {

  @Test
  public void testCallableFromTestCaller() {

    CallableCaller cc = new CallableCaller(new Callable());
    cc.doStuff();
  }

  @Test
  public void testCallableFromError() {

    ErrorCaller er = new ErrorCaller(new CallableCaller(new Callable()));

    try {

      er.doStuff();
      fail("Expected CallableFromError to be thrown");

    } catch (final CallableFromError cfe) {

      String expectedMessage
              = "org.adrianwalker.callablefrom.Callable "
              + "is not callable from "
              + "org.adrianwalker.callablefrom.ErrorCaller";
      String actualMessage = cfe.getMessage();

      assertEquals(expectedMessage, actualMessage);
    }
  }

  @Test
  public void testNotCallableFromSubclass() {

    CallableCallerSubclass ccs = new CallableCallerSubclass(new Callable());

    try {

      ccs.doStuff();
      fail("Expected CallableFromError to be thrown");

    } catch (final CallableFromError cfe) {

      String expectedMessage
              = "org.adrianwalker.callablefrom.Callable "
              + "is not callable from "
              + "org.adrianwalker.callablefrom.CallableCallerSubclass";
      String actualMessage = cfe.getMessage();

      assertEquals(expectedMessage, actualMessage);
    }
  }
}

Where CallableCaller can be called from implementations of TestCaller:

CallableCaller.java

package org.adrianwalker.callablefrom;

import org.adrianwalker.callablefrom.test.TestCaller;

public class CallableCaller {

  private final Callable callable;

  public CallableCaller(final Callable callable) {

    this.callable = callable;
  }

  @CallableFrom({
    @CallableFromClass(value=ErrorCaller.class, subclasses = false),
    @CallableFromClass(value=TestCaller.class, subclasses = true)
  })
  public void doStuff() {

    System.out.println("CallableCaller doing stuff");

    callable.doStuff(); // callable from here
  }
}

Usage

Using the callable-from library in a project requires the aspect to be weaved into your code at build time. Using Apache Maven, this means using the AspectJ plugin and specifying callable-from as a weave dependency:

pom.xml

<build>
  <plugins>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>aspectj-maven-plugin</artifactId>
      <version>1.11</version>

      <configuration>
        <complianceLevel>1.8</complianceLevel>
        <weaveDependencies>
          <weaveDependency>  
            <groupId>org.adrianwalker.callablefrom</groupId>
            <artifactId>callable-from</artifactId>
          </weaveDependency>
        </weaveDependencies>
      </configuration>

      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>test-compile</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Overhead

Checking every annotated method introduces significant overhead, I've bench-marked the same code compiled an run with and without the aspect weaved at compile time:

BenchmarkTest.java

package org.adrianwalker.callablefrom.example;

import java.util.Random;
import org.adrianwalker.callablefrom.CallableFrom;
import org.adrianwalker.callablefrom.CallableFromClass;
import org.junit.Test;

public final class BenchmarkTest {

  private static class CallableFromRandomNumberGenerator {

    private static final Random RANDOM = new Random(System.currentTimeMillis());

    @CallableFrom({
      @CallableFromClass(value = BenchmarkTest.class, subclasses = false)
    })
    public int nextInt() {

      return RANDOM.nextInt();
    }
  }

  @Test
  public void testBenchmarkCallableFrom() {

    long elapsed = generateRandomNumbers(1_000_000_000);

    System.out.printf("%s milliseconds\n", elapsed);
  }

  private long generateRandomNumbers(final int n) {

    CallableFromRandomNumberGenerator cfrng = new CallableFromRandomNumberGenerator();

    long start = System.currentTimeMillis();

    for (long i = 0; i < n; i++) {
      cfrng.nextInt();
    }

    long end = System.currentTimeMillis();

    return end - start;
  }
}

Without aspect weaving:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.adrianwalker.callablefrom.example.BenchmarkTest
13075 milliseconds

With aspect weaving:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.adrianwalker.callablefrom.example.BenchmarkTest
81951 milliseconds

13075 milliseconds vs 81951 milliseconds means the above code took 6.3 times longer to execute with @CallableFrom checking enabled. For this reason, if execution speed is important to you, I'd recommend only weaving the aspect for a test build profile and using another build profile, without the AspectJ plugin, for building your release artifacts (see the callable-from-usage project pom.xml for an example).

Conclusions

So is this the worst idea ever in the history of programming? Speed issues aside, it probably is because:

  1. I've never seen a language that offers this sort of method call enforcement as standard.
  2. An object in layer n, called by an object in layer n+1 should ideally contain no knowledge of the layer above it. The code could be changed to compare class object canonical name strings rather than the class object itself, so imports for calling classes are not needed in the callable class - but this creates a maintenance problem as refactoring tools won't automatically change the full class names in the string values and the compiler can't tell you if a class name does not exist.

That said, I still think something like this could help stop the proliferation of spaghetti code.

Source Code

The annotations and aspect code are provided in the callable-from project, with an example usage project similar to the diagram at the start of this post provided in the callable-from-usage project.