MethodLocal, Lazy Initialisierung von lokalen Variablen

“Progress isn’t made by early risers. It’s made by lazy men trying to find easier ways to do something.”

Robert A. Heinlein

Während der Implementierung des FakeInvocationInterceptors aus dem Beitrag Personas für Unit Tests zeigte sich eine Unschönheit im Code.

  @Override
public void interceptTestMethod(Invocation<Void> invocation, 
    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
  Parameter[] parameters = invocationContext.getExecutable().getParameters();
  for (int i = 0, n = parameters.length; i < n; i++) {
    Fake annotation = parameters[i].getAnnotation(Fake.class);
    if (annotation != null) {
      new FakePersonaBuilder("/personas.json").enable(EMAIL_BY_USERNAME, OVERRIDE_ZERO_VALUES)
      .ignore(annotation.ignore())
      .build(annotation.persona(), invocationContext.getArguments().get(i));
    }
  }
  invocation.proceed();
}

In den Zeilen 8 bis 10 wird für jeden annotierten Parameter des Aufrufs ein neuer FakePersonaBuilder erzeugt und mit ihm der Wert des Parameter modifiziert. Der Erstellen dieser Instanz ist relativ teuer, da eine JSON Datei für die Persona Definitionen eingelesen wird.

Einer erste Verbesserung war es, den teuren Teil der Initialisierung aus der Schleife herauszunehmen und innerhalb der Schleife mit einer Kopie zu erzeugen.

  @Override
public void interceptTestMethod(Invocation<Void> invocation, 
    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws  Throwable {
  Parameter[] parameters = invocationContext.getExecutable().getParameters();
  FakePersonaBuilder builder = new FakePersonaBuilder("/personas.json")
      .enable(EMAIL_BY_USERNAME, OVERRIDE_ZERO_VALUES);
  for (int i = 0, n = parameters.length; i < n; i++) {
    Fake annotation = parameters[i].getAnnotation(Fake.class);
    if (annotation != null) {
      builder.copy().ignore(annotation.ignore()).build(annotation.persona(), invocationContext.getArguments().get(i));
    }
  }
  invocation.proceed();
}

Schon ein Verbesserung, aber leider wird der FakInvocationInterceptor für jeden Test aufgerufen und erzeugt bei jedem Test einen FakePersonaBuilder, der oftmals überhaupt nicht verwendet wird. Die Lösung muss also sein, den FakePersonaBuilder erst bei seiner ersten Verwendung zu erzeugen. Traditionell eine weiteres If-Konstrukt in der Schleife.

  @Override
public void interceptTestMethod(Invocation<Void> invocation, 
    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws  Throwable {
  Parameter[] parameters = invocationContext.getExecutable().getParameters();
  FakePersonaBuilder builder = new FakePersonaBuilder("/personas.json")
      .enable(EMAIL_BY_USERNAME, OVERRIDE_ZERO_VALUES);
  for (int i = 0, n = parameters.length; i < n; i++) {
    Fake annotation = parameters[i].getAnnotation(Fake.class);
    if (annotation != null) {
       if (builder == null) {
           builder = new FakePersonaBuilder("/personas.json")
              .enable(EMAIL_BY_USERNAME, OVERRIDE_ZERO_VALUES);
      }
      builder.copy().ignore(annotation.ignore()).build(annotation.persona(), invocationContext.getArguments().get(i));
    }
  }
  invocation.proceed();
}

Besser wäre eine Lösung, die an die ThreadLocal Klasse aus dem JDK angelehnt ist. Diese Klasse hält für jeden Thread eine eigene Kopie einer Klasse bereit und kann diese auch erst bei Bedarf initialisieren. Mit einer entsprechenden eigenen MethodLocal Klasse, sieht der obige Code wie folgt aus.

  @Override
public void interceptTestMethod(Invocation<Void> invocation, 
    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws  Throwable {
  Parameter[] parameters = invocationContext.getExecutable().getParameters();
  MethodLocal<FakePersonaBuilder> builder = new MethodLocal<>(
      () -> new FakePersonaBuilder("/personas.json").enable(EMAIL_BY_USERNAME, OVERRIDE_ZERO_VALUES)
  );
  for (int i = 0, n = parameters.length; i < n; i++) {
    Fake annotation = parameters[i].getAnnotation(Fake.class);
    if (annotation != null) {
      builder.get().copy().ignore(annotation.ignore()).build(annotation.persona(), invocationContext.getArguments().get(i));
    }
  }
  invocation.proceed();
}

Die MethodLocal Klasse wird mit einem Supplier aufgerufen, der Instanzen der gewünschten Klasse erzeugen kann. Wird die Methode builder.get() das erste Mal aufgerufen, dann wird mit dem Supplier eine neue Instanz erzeugt und zur Verfügung gestellt. Bei jedem weiteren Aufruf wird die initial erzeugte Instanz zurück gegeben.

public class MethodLocal<T> {
  private final Supplier<? extends T> supplier;
  private T value;

  public MethodLocal(Supplier<? extends T> supplier) {
    this.supplier = Objects.requireNonNull(supplier);
  }

  public T get() {
    if (value == null) {
      value = supplier.get();
    }
    return value;
  }
}

Die MethodLocal Klasse ist mit wenigen Zeilen implementiert und kann zum lazy Initialisieren beliebiger Klassen innerhalb einer Methode genutzt werden. Sie ist nicht schneller als die traditionellen Lösungen, aber sie verbessert die Lesbarkeit des Sourcecode.