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:
- I've never seen a language that offers this sort of method call enforcement as standard.
- 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
- Code available in GitHub - callable-from
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.