czwartek, 26 września 2013

Fluent builder do generowania fluent builderów cz. 3

Opisany w części 2 generator buidlerów został gruntownie zrefaktoryzowany i ewoluował o poziom wyżej, jego użycie staje się jeszcze łatwiejsze. Korzystając z Annotation Processing Tool, mechanizm generowania klas został przeniesiony do procesora adnotacji, którego możemy na różne sposoby podłączyć do projektu.

Nie nudząc technicznymi szczegółami przejdźmy do przykładu. Klasę np. Order oznaczamy adnotacją @GenerateBuilder, która informuje procesor, że dla niej ma zostać wygenerowany builder:
@GenerateBuilder
public class Order {
 private List<OrderItem> items;
 private Date createDate;
 private boolean realized;

 public boolean isRealized() {
  return realized;
 }
 public void orderRealized() {
  // very complex implementation
  realized = true;
 }
}
Żeby w jakiś sposób automatycznie aktualizować istniejące buildery o nowe pola, które doszły w klasie Order (lub odpowiednio usuwać metody inicjujące nieistniejące pola), builder został rozbity na 2 klasy. Jedna z nich jest aktualizowana zawsze przy uruchomieniu procesora, druga generowana tylko raz, dzięki czemu możemy do niej dodawać własne metody budujące. Dla klasy Order powstaną następujące klasy:
public abstract class AbstractOrderBuilder<B> extends AbstractBuilder<Order, B> {
 public abstract B withItems(List<OrderItem> items);
 public abstract B withCreateDate(Date createDate);
 public abstract B withRealized(boolean realized);
 public B withItems(OrderItem... items){
  return withItems(new ArrayList<OrderItem>(Arrays.asList(items)));
 }
}
public abstract class OrderBuilder extends AbstractOrderBuilder<OrderBuilder> {
 public static OrderBuilder anOrder(){
  return AbstractBuilderFactory.createImplementation(OrderBuilder.class);
 }
}
Samo użycie może wyglądać następująco:
@Test
public void shouldCreateRealizedOrder() {
 // when
 Order order = anOrder().withRealized(true).build();
 // then
 assertTrue(order.isRealized());
}
Klasę OrderBuilder można rozszerzać, wywołując rzeczywiste metody domenowe, np:
public abstract class OrderBuilder extends AbstractOrderBuilder<OrderBuilder> {
 public static OrderBuilder create() {
  return AbstractBuilderFactory.createImplementation(OrderBuilder.class);
 }
 public OrderBuilder realized() {
  Order order = targetObject();
  // invoking real domain method
  order.orderRealized();
  // other methods
  return builder();
 }
}
@Test
public void shouldCreateRealizedOrder() {
 // when
 Order order = anOrder().realized().build();
 // then
 assertTrue(order.isRealized());
}
Do podłączenia procesora do projektu może zostać wykorzystany ant, maven, eclipse. Jest również możliwość użycia generatora w starym stylu, czyli wygenerowania ciała klasy buildera do konsoli.

Procesor może również poszukiwać klas z adnotacjami JPA (@Entity, @Ebeddable, @MappedSuperclass).

Do poznania szczegółów odsyłam do wiki projektu. Zachęcam do forkowania i dzielenia się uwagami.

5 komentarzy:

  1. Odpowiedzi
    1. Nie widzę związku, lombok jedyne co robi, to generuje za Ciebie gettery i settery (plus kilka innych metod)?

      Usuń
  2. Ciekawy projekt. Ostatnio też bawię się z generatorami kodu opartymi o Annotation Processing Tool.
    Tak więc przyda mi się twa praca w poszukiwaniu inspiracji w alternatywnym podejściu do tego.

    Chyba jest błąd w klasie ProcessorContext, zamist:
    public static final String FLUENT_BUILDER_ANNOTATATION = "info.ludwikowski.processor.GenerateBuilder";
    Powinno być:
    public static final String FLUENT_BUILDER_ANNOTATATION = "info.ludwikowski.annotation.GenerateBuilder";


    Wynika to zapewne z tego że w anotacjach nie można byłoby użyć tej konstrukcji jeśli byłoby to wyrażone tak:
    public static final String FLUENT_BUILDER_ANNOTATATION = GenerateBuilder.class.getName();

    Ciekawe czy użycie, właśnie Annotation Processing Tool do gerowania słownika takich zmiennych nie byłoby dobrym rozwiązaniem do obejścia tego problemu?

    Ciekawi mnie też jak pracowałeś z api z pakietów "javax.lang.model"?, z własnego doświadczenia wiem że debugowanie tego kodu może być uciążliwe.

    OdpowiedzUsuń
    Odpowiedzi
    1. Dzięki za uwagę, robiłem przepakietowanie żeby można było tego w pracy używać w kontenerze osgi, poprawiłem 1.0.6. Co dziwne działało bez problemu:)

      Jeśli chodzi o debugowanie to jedną opcją jest opakowanie tego "pluginem" do eclipsa, brzmi dość strasznie, ale nie jest to aż takie trudne. Wzorowałem się na tych materiałach:
      http://kerebus.com/2011/02/using-java-6-processors-in-eclipse/
      https://code.google.com/p/acris/wiki/AnnotationProcessing_DebuggingEclipse
      http://vimeo.com/8876665

      Usuń
    2. Dzięki za linki. Szczególnie ten ostatni jest pomocny. Jak już się eclispa uruchomi, to
      nie trzeba restartować go z każdą zmianą - to jest zwłaszcza pomocne przy poznawaniu tego api.

      Ja znalazłem w sieci podejście z zastosowaniem kompilacji podczas testów, co też jest pomocne.
      Wzorowałem się na tym przykładzie:
      https://svn.java.net/svn/glassfish~svn/trunk/logging-annotation-processor/src/test/java/org/glassfish/logging/annotation/LogMessagesResourceBundleGeneratorTest.java

      Usuń