Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Step builder pattern using delegation and enums?

I have this project I'm working on and basically this is what I would like to achieve.

This is what I have:

MyObject obj = MyObject.builder()
                       .withValue("string")
                       .withAnotherValue("string")
                       .build();

MyObject obj = MyObject.builder()
                       .withValue("string")
                       .withAnotherValue("string")
                       .withField("key", "value")
                       .build();

So the step builder pattern forces the user to use the withValue() method and the withAnotherValue() method in that order. The method field() is optional and can be used as many times as you want.I followed this website for example http://www.svlada.com/step-builder-pattern/

So what I would like to achieve is this:

MyObject obj = MyObject.builder(Type.ROCK)
                       .withColour("blue")
                       .withValue("string")
                       .withAnotherValue("string")
                       .build();

MyObject obj = MyObject.builder(Type.STONE)
                       .withWeight("heavy")
                       .withValue("string")
                       .withAnotherValue("string")
                       .withField("key", "value")
                       .build();

So in the builder() method you'd put an enum type and based on the enum you'd have a different set of methods appear. So for ROCK the withValue(),withAnotherValue() and withColour() are now mandatory. But for STONE withWeight(), withAnotherValue() and withColour() are mandatory.

I something like this possible? I have been trying for the past two days to figure this out but I just can't seem to get it to give specific methods for each type. It just shows all the methods in the Builder.

Any thoughts and help is much appreciated.

Code:

Enum

public enum Type implements ParameterType<Type> {

  ROCK, STONE

}

ParameterType

interface ParameterType<T> {}

MyObject

public class MyObject implements Serializable {

  private static final long serialVersionUID = -4970453769180420689L;

  private List<Field> fields = new ArrayList<>();

  private MyObject() {
  }

  public interface Type {

    Value withValue(String value);
  }

  public interface Value {

    Build withAnotherValue(String anotherValue);
  }

  public interface Build {

    MyObject build();
  }

  public Type builder(Parameter type) {
    return new Builder();
  }

  public static class Builder implements Build, Type, Value {

    private final List<Field> fields = new ArrayList<>();

    @Override
    public Build withAnotherValue(String anotherValue) {
      fields.add(new Field("AnotherValue", anotherValue));
      return this;
    }

    @Override
    public Value withValue(String value) {
      fields.add(new Field("Value", value));
      return this;
    }

    @Override
    public MyObject build() {
      MyObject myObject = new MyObject();
      myObject.fields.addAll(this.fields);
      return myObject;
    }
  }

}
like image 944
Hexcii Avatar asked Nov 07 '22 08:11

Hexcii


1 Answers

This isn't possible using enum, but you could do this with a custom enum-like class:

public final class Type<B extends MyObject.Builder> {
    private final Supplier<? extends B> supplier;

    private Type(Supplier<? extends B> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    public B builder() {
        return supplier.get();
    }

    public static final Type<MyObject.RockBuilder> ROCK =
        new Type<>(MyObject.RockBuilder::new);

    public static final Type<MyObject.StoneBuilder> STONE =
        new Type<>(MyObject.StoneBuilder::new);
}

public class MyObject {
    // ...

    // And this method is probably superfluous at this point.
    public static <B extends MyObject.Builder> builder(Type<? extends B> type) {
        return type.builder();
    }
}

You could adapt that approach to a step builder easily, but there's a separate issue here. Since each step in a step builder specifies the next step in the return type, you can't re-use step interfaces very easily. You would need to declare, for example, separate interfaces RockValueStep, StoneValueStep, etc. because the interfaces themselves specify the step order.

The only simple way around that would be if the separate types (rock, stone, etc.) only strictly added steps such that e.g. Type.ROCK returns a ColourStep and Type.STONE returns a WeightStep, and both ColourStep and WeightStep return ValueStep:

// Rock builder starts here.
interface ColourStep { ValueStep withColour(String c); }
// Stone builder starts here.
interface WeightStep { ValueStep withWeight(String w); }
// Shared.
interface ValueStep { AnotherValueStep withValue(String v); }

And then:

public final class Type<B /* extends ABuilderStepMarker, possibly */> {
    // (Constructor and stuff basically same as before.)

    public static final Type<MyObject.ColourStep> ROCK =
        new Type<>(/* implementation */::new);

    public static final Type<MyObject.WeightStep> STONE =
        new Type<>(/* implementation */::new);
}

The reasons this kind of thing can't be done using enum are pretty much:

  • enum can't be generic:

    // This is an error.
    enum Type<T> {
    }
    
  • Although you could declare an abstract method on an enum and override it with a covariant return type, the covariant return type is never visible:

    // This is valid code, but the actual type of
    // Type.ROCK is just Type, so the return type of
    // Type.ROCK.builder() is just MyObject.Builder,
    // despite the override.
    enum Type {
        ROCK {
            @Override
            public MyObject.RockBuilder builder() {
                return new MyObject.RockBuilder();
            }
        };
        public abstract MyObject.Builder builder();
    }
    
like image 120
Radiodef Avatar answered Nov 14 '22 22:11

Radiodef