Dienstag, 1. November 2011

Ein Codegenerator für POJO-Builders


Seit Ende September beschäftige ich mich auf Github mit einem Open-Source-Projekt mit dem Namen PojoBuilder.

Ich entwickle dort einen Java-Codegenerator, der für ein beliebiges POJO einen passenden Pojo-Builder erzeugen kann. Dieser bietet die folgende Funktionalitäten:

  • ein "fluent interface" zum Festlegen der Eigenschaftswerte des POJOs
  • und eine build-Methode für die Erstellung einer neuen POJO Instanz mit diesen Werten
Unter anderem eignet sich so ein Builder besonders für die Erstellung von Testdaten im Rahmen von automatisierten Tests, wie das Nat Pryce anschaulich beschrieben hat.

Ein kleines Beispiel

Der Codegenerator lässt sich naheliegenderweise am einfachsten anhand eines Code-Beispiels erläutern. Nehmen wir an, wir haben die folgende Javaklasse erstellt und benötigen dafür einen Builder.
public class Contact { 
    private final String name;
    private String email;

    public Contact(String aName) {
        this.name = aName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }
}

Der dazugehörige Builder sollte beispielsweise auf die folgende Weise bedient werden können:

Contact james = new ContactBuilder()
    .withName("James Bond")
    .withEmail("007@secretservice.org")
    .build();

Mit dem Einsatz von PojoBuilder lässt sich dieses Ziel durch die Markierung der Klasse mit zwei Annotations erreichen.

@GeneratePojoBuilder(intoPackage = "samples.builder")
public class Contact { 
    private final String name;
    private String email;

    @ConstructorProperties({ "name"})
    public Contact(String aName) {
        this.name = aName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }
}

Mit @GeneratePojoBuilder wird der Codegenerator angewiesen, für die annotierte Klasse einen Builder zu generieren. Das Attribut intoPackage ist optional und bestimmt das Java-Package, in das der Builder generiert werden soll. Mit @ConstructorProperties legt man fest, welcher Konstruktor verwendet werden soll und wie die Parameter auf Properties abgebildet werden.

Der generierte Builder sieht dann so aus (Kommentare habe ich entfernt):

public class ContactBuilder implements Cloneable {

   protected String value$email$java$lang$String; 
   protected boolean isSet$email$java$lang$String = false; 

   protected String value$name$java$lang$String; 
   protected boolean isSet$name$java$lang$String = false; 

   public ContactBuilder() {
   }

   public ContactBuilder withEmail( String value) {
      this.value$email$java$lang$String = value;
      this.isSet$email$java$lang$String = true;
      return this;
   }

   public ContactBuilder withName( String value) {
      this.value$name$java$lang$String = value;
      this.isSet$name$java$lang$String = true;
      return this;
   }

   public Object clone() {
      try {
            ContactBuilder result = (ContactBuilder)super.clone();
            return result;
      } catch (CloneNotSupportedException e) {
            throw new InternalError(e.getMessage());
      }        
   }
   
   public ContactBuilder but() {
      return (ContactBuilder)clone();
   }

   public Contact build() {
      Contact result = new Contact( this.value$name$java$lang$String );
      if ( this.isSet$email$java$lang$String) {    
           result.setEmail( this.value$email$java$lang$String);  
      }
      return result;
   }
}

Wie ist das ganze nun implementiert?

Für die eigentliche Codegenerierung nutze ich, wie schon in früheren Projekten, die Stringtemplate Engine. Offen war ursprünglich lediglich die Frage, wie der Generator angestoßen werden sollte. Ich wollte erreichen, dass er sowohl in einem Build-Lauf als auch in einer IDE genutzt werden kann. Da sie sich für beides sehr gut eignet, habe ich mich für die Annotation Processor API entschieden, die seit Java 5 verfügbar ist. Somit war auch festgelegt, dass der Generator über Annotations gesteuert wird (und nicht etwa über XML oder eine andere Beschreibungssprache).

Automatisierte Tests

Eine Schwierigkeit im Projekt ist allerdings die Frage, wie man einen Annotation-Processor ordentlich testet, und zwar im Rahmen von Acceptance-Tests sowie Unit-Tests (ja, ich gebe zu, die Tests nicht vorher geschrieben zu haben). Bislang meine ich immerhin einen guten Weg für die Acceptance-Tests gefunden zu haben, und zwar mittels einer JavaProject-Fassade für die Java Compiler API, mit der ich tatsächlich on-the-fly ein kleines Java-Projekt komplett mit Sourcen erstellen und kompilieren kann. Anschließend kann ich in meinem TestCase die generierten Klassen per Classloader laden und mittels Reflection prüfen, oder alternativ dazu, könnte ich auch vorbereitete Unit-Tests laufen lassen, welche die generierten Builder testen.

Was allerdings das Unit-Testen des AnnotationProcessor angeht, tappe ich noch im Dunkeln. Grundsätzlich bin ich ein ein Fan von Interaction-Based-Testing, allerdings erscheint mir in diesem Fall mit Mock-Objekten zu arbeiten sehr aufwändig. Alternativ dazu bleibt wohl nur ein State-Based Ansatz, wie etwa in diesem Post auf Stackoverflow beschrieben.

Keine Kommentare:

Kommentar veröffentlichen