1

I have a class hierarchy with a single abstract base class and several child classes. The base class has ~25 fields and each child has an additional number 0-8 fields.

I would like to use the Builder pattern to construct each child instance and I'd like to use Lombok as much as possible to keep the code concise. Following this suggestion I have the code as below:

@AllArgsConstructor
@Data
public abstract class Base {
   private int b1, b2, ... , b25;
}

public class C1 extends Base {
   private int c11, c12, ... , c16;

   @Builder
   private C1(int b1, int b2, ..., int b25, int c11, ... int c16) {
       super(b1, b2, ...., b25);
       this.c11 = c11;
       ...
       this.c16 = c16;
   }
}

public class C2 extends Base {

   @Builder
   private C2(int b1, int b2, ..., int b25) {
       super(b1, b2, ...., b25);
   }
}

This makes it easy to construct the child classes as

C1 c1 = C1.builder().b1(1).b2(2)....b25(25).c11(101).c12(102).build();
C2 c2 = C2.builder().b1(1).b2(2)....b25(25).build();

The problem is that the the .b1().b2()... chained calls are repeated every time any child class is created.

Ideally, I want a common way to set the B values regardless of which child class is being built. (Let's assume there's another class called BValuesProvider that can supply those values)

public void setBValues(BValuesProvider bv, // what else goes here??? //) {
    // something.b1(bv.b1()).b2(bv.b2()) ...
}

public createC1(BValuesProvider bv, c11, c12, ..., c16) {
    C1.Builder c1b = C1.builder().c11(c11).c12(c12)....c16(c16);
    // Call setBValues somehow
    return c1b.build();
}

public createC2(BValuesProvider bv) {
    // Call setBValues somehow
    return c2b.build();
}

My current solution has been to attach the @Data annotation to the base class to expose setters/getters so my code looks like this:

public void setBValues(BValuesProvider bv, Base cx) {
    cx.setB1(bv.b1());
    cx.setB2(bv.b2());
    ...
    cx.setB25(bv.b25());
}

public createC1(BValuesProvider bv, c11, c12, ..., c16) {
    C1 c1 = C1.builder().c11(c11).c12(c12)....c16(c16).build();
    setBValues(bv, c1);
    return c1;
}

public createC2(BValuesProvider bv) {
    C2 c2 = C2.builder().build();
    setBValues(bv, c2);
    return c2;
}

Questions:

  • Is there a better way to do this? Specifically, I feel that first building a child class (fully) and then calling setBxx() functions on it seems like a bad pattern. Exposing the setters itself makes the class quite mutable.

  • There have been other questions on SO about builders/inheritance

    However none of them talk about having a "base builder" that each child builder is a sub-class of. So, I can't figure out using generics, what the second argument to the setBValues function should be.

  • I also tried Lombok's @Superbuilder annotation but again, while it greatly simplifies the code, I still don't see how to get a base builder.
user2602740
  • 637
  • 6
  • 17
  • If you use `@SuperBuilder`, can't you simply create a method (maybe in your `BValuesProvider` class) with the abstract `BaseBuilder` as parameter, and call the builder's setters in it? – Jan Rieke Aug 12 '19 at 18:09
  • Not sure I understand how I could do this - can you please show me a small example? Also, I should mention that `BValuesProvider` is not a class that I can necessarily modify since it belongs to a different package. I suppose I could add another interface class for this purpose(?). – user2602740 Aug 12 '19 at 18:42

2 Answers2

2

This can be achieved using the (experimental) @SuperBuilder annotation and lombok >= 1.18.4. You can customize the @SuperBuilder of Base by adding a method that takes a BValuesProvider as argument and sets all values from that:

@SuperBuilder
public abstract class Base {
    public static abstract class BaseBuilder<C extends Base, B extends BaseBuilder<C, B>> {
        public B fillFromProvider(BValuesProvider bv) {
            b1(bv.b1());
            b2(bv.b2());
            ...
            return self();
        }
    }
    ...
}

Then you can use it like this (where bv is a BValuesProvider instance):

C1 c1 = C1.builder().fillFromProvider(bv).c11(11).build();
Jan Rieke
  • 7,027
  • 2
  • 20
  • 30
  • I think this will work, thanks! My only concern is that my code is meant to be a library that is used by other packages. If I go with this approach, my library will need to import (and be aware of) things like `BValuesProvider` that are classes belonging to other packages (and completely unrelated to my library). – user2602740 Aug 13 '19 at 14:54
  • Well, you could move the `fillFromProvider` method out of the builder to some other class. However, either the builder has to know the provider, or the other class (e.g. the provider) has to know the builder. I don't see a way around that. – Jan Rieke Aug 13 '19 at 15:34
0

OP here. While this isn't an answer to the exact question I asked, I wanted to share an alternative that still uses Lombok but not @Builder.

@Getter
public abstract class Base<T extends Base> {
   private int b1, b2, ... , b25;
   private T cast() { return (T) this; }
   public T setB1(final int b1) { this.b1 = b1; return cast(); }
   public T setB2(final int b2) { this.b2 = b2; return cast(); }
   ...
}

@Getter @Setter @Accessors(chain = true)
public class C1 extends Base<C1> {
   private int c11, c12, ... , c16;
   public static C1 init() { return new C1(); }
   private C1() {}
}

@Getter @Setter @Accessors(chain = true)
public class C2 extends Base<C2> {
   public static C2 init() { return new C2(); }
   private C2() {}
}

I've just generified the base class and used a chained accessor on the child classes. The callers would then be modified as:

public void setBValues(BValuesProvider bv, Base cx) {
    cx.setB1(bv.b1())
      .setB2(bv.b2())
      ...
      .setB25(bv.b25());
}

public createC1(BValuesProvider bv, c11, c12, ..., c16) {
    C1 c1 = C1.init().setC11(c11)....setC16(c16);
    setBValues(bv, c1);
    return c1;
}

public createC2(BValuesProvider bv) {
    C2 c2 = C2.init();
    setBValues(bv, c2);
    return c2;
}

Advantages:

  • my code needs to have no knowledge of BValuesProvider
  • setting the common base params seems more natural to me in that chained fashion

Disadvantages:

  • base class is a bit more verbose but overall, it's not so bad because we're avoiding that big constructor in each child class.
  • it's not really the Builder pattern because we're creating the object and doing a bunch of set()s on it vs. doing a .builder().x(x).y(y).build() on it.
user2602740
  • 637
  • 6
  • 17