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

2

24

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?

Sherri answered 24/7, 2014 at 14:14 Comment(5)
This seems like a really hard task, and at first I would have said it's impossible, but based on how Project Lombok at projectlombok.org is able to generate setters based on annotations, it seems unlikely that you can't somehow make a @Maker(Pojo.class) annotation that would allow the system to generate the setters. But I don't know how this works, all I know is this is quarter step.Absorbent
Mockito and Spring do this type of thing when creating mocks or proxies. Look at how they do it.Killoran
@Zhuinden Yes, I've heard of Lombok. I thought it might be possible without an extra compiler pass.Sherri
@JohnB Spring is to large to look it up and I have not had much experience with it, yet. But I thought Mockito might be feasible to look into and I have some things done with it, already. Thanks for the opinion, I thought that as well -- since Mockito can do this to, it appears.Sherri
@JohnB: Mockito and Spring don't do anything like this. Creating a proxy, where the public interface is the same as the class you're delegating to/mocking, is much different than generating an entirely different public API and expecting it to be available at compile time. At the end of the day, a mocked or proxied class still is an instance-of that class/interface (i.e. you declare it as the delegate class). A PojoBuilder is most certainly not.Quibbling
H
88

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();
Hendrik answered 30/4, 2021 at 12:12 Comment(0)
S
2

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 + "]";
  }    
}
Shari answered 24/7, 2014 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.