Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a builder class using Generics, not annotations?

I want to write a generic builder class which wraps around any java class and providing setter functions of a specific style. I am not sure if this could be called "dynamically generated functions".

When I have a beanish Pojo class i.e.

class Pojo {
    public void setValue(int value) {...}
    public void setName(String name) {...}
}

My Maker class should be usable like this:

Pojo p = Builder<Pojo>.create(new Pojo())
             .setName("Funny")
             .setValue(123)
             .build();

As you can see, the work it does should be similar to

class PojoBuilder {
    private Pojo pojo;
    PojoBuilder(Pojo pojo) { this.pojo = pojo; }
    public static PojoMaker create(Pojo p) { return new PojoBuilder(p); }
    public PojoBuilder setName(String name) { pojo.setName(name); return this; }
    public PojoBuilder setValue(int val) { pojo.setValue(val); return this; }
    public Pojo make() { return pojo; }
}

Only, I would like Maker to be generic. Obviously, the "setXyz"-Methods depend on the generic argument. How to do that?

Of course, functionally equivalent but syntactically different approach is also fine.

I'd like to do it without annotations: With annotations I gather I'd need a second javac-pass over my source code, generating the wrapper code. That seems to be what Limbok does or how some JPA wrappers work. But when I work with Mockito it seems that this pass is not necessary. So, How can I do it with Generics?

like image 332
towi Avatar asked Jul 24 '14 14:07

towi


2 Answers

The easiest way is using Lombok library.

@Value
@Builder
public class MessageEvent<T> {
    T message;
    TypeEnum type;
}

And the usage is like this.

MessageEvent<String> messageEvent = MessageEvent.<String>builder().message("test").build();
like image 117
Ali Esmaily Avatar answered Sep 18 '22 13:09

Ali Esmaily


The following code shows that it is possible up to a certain point (I did not test for corner case such as primitive type, etc).

It use Java 8, lamdba and type erasure.

Since in Java 8 you can reference constructor using X::new, and method using the same syntax, it works by stacking into a map each method and their parameter, so that we don't rely on a particular instance (so that build() can create new instance of Foobar).

package foobar;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

public class Maker<T> {
  private final Supplier<? extends T> supplier;
  /**
   * We need to store the instance since build must return a new instance.
   * <p>
   * Sadly, we need to rely on type erasure (hence BiConsumer, not BiConsumer<T,V>).
   */
  @SuppressWarnings("rawtypes")
  private final Map<BiConsumer, Object> values = new HashMap<>();

  public Maker(final Supplier<? extends T> supplier) {
    this.supplier = supplier;
  }

  public static <T> Maker<T> create(final Supplier<? extends T> builder) {
    return new Maker<>(builder);
  }

  public <U> Maker<T> set(final BiConsumer<T, U> consumer, final U value) {
    values.put(consumer, value);
    return this;
  }

  @SuppressWarnings("unchecked")
  public T create() {
    final T instance = supplier.get();

    values.forEach((key, value) -> {
      key.accept(instance, value);
    });

    return instance;
  }

  public static void main(final String[] args) {
    final Maker<Foobar> maker = Maker.create(Foobar::new).set(Foobar::setName, "Name");

    final AtomicInteger generator = new AtomicInteger(0);
    Arrays.asList("Alpha", "Beta", "Gamma").forEach(name -> {
      final Integer id = generator.incrementAndGet();

      maker.set(Foobar::setName, name);
      maker.set(Foobar::setId, id);
      final Foobar foobar = maker.create();

      if (!name.equals(foobar.getName())) {
        throw new AssertionError("expected " + name + ", got " + foobar.getName());
      }
      if (!id.equals(foobar.getId())) {
        throw new AssertionError("expected " + id + ", got " + foobar.getId());
      }

      System.out.println(foobar);

    });

  }          
}

With the Foobar class:

public class Foobar {
  private Integer id;
  private String name;    
  public Integer getId() {return id;}
  public void setId(final Integer id) {this.id = id;}    
  public String getName() {return name;
  public void setName(final String name) {this.name = name;}    
  @Override public String toString() {
    return "Foobar [id=" + id + ", name=" + name + "]";
  }    
}
like image 39
NoDataFound Avatar answered Sep 22 '22 13:09

NoDataFound