Java – Possible way to make java class builder more abstract by using interface required keys

design-patternsjava

I'm looking for a more abstract pattern for builders that handles required
fields without the need of writing a validator that checks if all requried fields are set.

I like this builder. But is there a way to make the interfaces more abstract
so that I don't need to create a new interface for each required field?

public class Example {

    private String first;
    private String second;
    private String third;
    private String fourth;
    private String fifth;

    public static RequiredSecond builder(String first) {
        return new ExampleBuilder(first);
    }

    public interface RequiredSecond {
        RequiredThird withSecond(String second);
    }

    public interface RequiredThird {
        RequiredFourth withThird(String third);
    }

    public interface RequiredFourth {
        Build withFourth(String fourth);
    }

    public interface Build {
        Build withFifth(String fifth);

        Example build();
    }

    private static class ExampleBuilder implements RequiredSecond, RequiredThird, RequiredFourth, Build {
        Example example = new Example();

        public ExampleBuilder(String first) {
            example.setFirst(first);
        }

        @Override
        public Build withFifth(String fifth) {
            example.setFifth(fifth);
            return this;
        }

        @Override
        public Example build() {
            return example;
        }

        @Override
        public Build withFourth(String fourth) {
            example.setFourth(fourth);
            return this;
        }

        @Override
        public RequiredFourth withThird(String third) {
            example.setThird(third);
            return this;
        }

        @Override
        public RequiredThird withSecond(String second) {
            example.setSecond(second);
            return this;
        }
    }

    public String getFirst() {
        return first;
    }

    public void setFirst(String first) {
        this.first = first;
    }

    public String getSecond() {
        return second;
    }

    public void setSecond(String second) {
        this.second = second;
    }

    public String getThird() {
        return third;
    }

    public void setThird(String third) {
        this.third = third;
    }

    public String getFourth() {
        return fourth;
    }

    public void setFourth(String fourth) {
        this.fourth = fourth;
    }

    public String getFifth() {
        return fifth;
    }

    public void setFifth(String fifth) {
        this.fifth = fifth;
    }

    @Override
    public String toString() {
        return "Example [first=" + first + ", second=" + second + ", third=" + third + ", fourth=" + fourth +
                ", fifth=" + fifth + "]";
    }

}
public class Labb {

    public static void main(String[] args) {
        // All required including optional
        Example ex1 = Example.builder("first").withSecond("second").withThird("third").withFourth("fourth").withFifth("optional fith").build();
        System.out.println(ex1);
        // Output: Example [first=first, second=second, third=third, fourth=fourth, fifth=optional fith]

        // All required excluding optional
        Example ex2 = Example.builder("first").withSecond("second").withThird("third").withFourth("fourth").build();
        System.out.println(ex2);
        // Output: Example [first=first, second=second, third=third, fourth=fourth, fifth=null]
    }
}

Best Answer

According to Joshua Bloch, author of Effective Java, item 2 basically says there are three common ways to create objects in the language:

  1. User overloaded constructors to set required fields, and mutators to set optional fields.

    public class Example {
      private String first;
      private String second;
    
      //first is only required field
      public Example(final String first){
        this.first = first;
      }
    
      //Can't make a constructor just for second field
    
      //first and second are both required fields
      public Example(final String first, final String second){
        this.first = first;
        this.second = second;
      }
      //mutators for optional fields
    }
    
  2. Use JavaBeans style mutators and accessors, not concerned too much about mutability enforcement at compile time.

    public class Example {
      private String first;
    
      public void setFirst(final String first){
        this.first = first;
      }
    
      public String getFirst(){
        return first;
      }
    }
    
  3. Use the Builder design pattern

    public class Example {
      private String first;
      private String second;
    
      private Example(Builder builder){
        this.first = builder.first;
        this.second = builder.second;
      }
    
      @Override
      public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Example [first=").append(first).append(", second=").append(second).append("]");
        return sb.toString();
      }
    
      //Add getter methods as needed, but no setters if you want immutable objects
    
      public static class Builder{
        private String first;
        private String second;
    
        //required fields go in the constructor
        public Builder(final String first){
          this.first = first;
        }
    
        //optional fields have setter type methods
        public Builder second(String value){
          this.second = value;
        }
    
        public Example build(){
          return new Example(this);
        }
      }
    
      public static void main(String[] args){
        //builder with required first field
        Builder builder = new Example.Builder("first");
        //optional second field - calls can be chained
        builder.second("second");
        Example example = builder.build();
        System.out.println(example.toString());
      }
    }
    

The primary advantage of using the Builder pattern is that all built objects, instances of Example in this case, can be guaranteed immutable at compile time.

It is not clear to me why you have an interface for each field in the final built object, nor why they are public interfaces. This appears to me to be incorrect, but I am not sure.