javabuilderbuilder-pattern

How to create multiple varieties of a Class using the Builder Pattern?


My goal is to use the Builder Pattern to create the Product Details' field based on the product category.

Regardless of the product category, this is the basic ProductDetails attributes.

int stock;          // required
String ships_from;  // required

String brand;       // optional

Computer category could have

String model;           // optional
String screen_size;     // optional
String warranty_period; // optional

Food category could have

String shelf_life;      // required

String expiration_date; // optional

This is the basic builder pattern I use

public class ProductDetails {
  // attributes

  private ProductDetails(ProductDetailsBuilder builder) {
    this.attribute1 = builder.attribute1;
    this.attribute2 = builder.attribute2; // and so on.
  }

  // getters

  public static class ProductDetailsBuilder {
    // attributes

    public ProductDetailsBuilder(//required attributes) {
      this.attribute1 = attribute1;
    }

    // setters

    public ProductDetails build() { return new ProductDetails(this); }
  }
}

The problem arise when I try to extends the ProductDetails class to e.g ProductDetails_Computer or ProductDetails_Food class.

public class ProductDetails_Computer extends ProductDetails {
  // attributes

  private ProductDetails_Computer(ProductDetails_ComputerBuilder builder) {
    this.attribute1 = builder.attribute2;
  }

  // getters

  public static class ProductDetails_ComputerBuilder {
    // attributes.

    // setters

    public ProductDetails_Computer build() { return new ProductDetails_Computer(this); }
  }
}

My expected result: I can do public class ProductDetails_Computer extends ProductDetails.

My actual result: Because the ProductDetails constructor is private/protected, I can't extends. Some websites forbid the use of public constructor to avoid direct initialization, and I agree with this practices.


Solution

  • Effective Java suggests using parallel hierarchy of builders for such cases.

    Here is how abstract and concrete classes may look like:

    ProductDetails (Base Abstract class):

    abstract public class ProductDetails {
        int stock;
        String ships_from;
    
        String brand;
        
        // Recursive generics
        abstract static class Builder<T extends Builder<T>> {
            int stock;
            String ships_from;
    
            String brand;
    
            public Builder(int stock, String ships_from) {
                this.stock = stock;
                this.ships_from = ships_from;
            }
    
            // returns the reference of implementation (child class)
            T brand(String brand) {
                this.brand = brand;
                return self();
            }
            
            // force implementation to return child class' builder
            protected abstract T self();
             
           // force implementation to return itself while holding "Code to interface" practice
            abstract ProductDetails build();
        }
    
        protected ProductDetails(Builder<?> builder) {
            this.stock = builder.stock;
            this.ships_from = builder.ships_from;
    
            this.brand = builder.brand;
        }
        
    }
    

    Implementations may look like:

    ProductDetails_Computer

    public class ProductDetails_Computer extends ProductDetails {
        String model;
        String screen_size;
        String warranty_period;
    
        private ProductDetails_Computer(Builder builder) {
            super(builder);
            this.model = builder.model;
    
            this.screen_size = builder.screen_size;
            this.warranty_period = builder.warranty_period;
        }
    
        public static class Builder extends ProductDetails.Builder<Builder> {
            String model;
            String screen_size;
            String warranty_period;
    
            public Builder(int stock, String ships_from) {
                super(stock, ships_from);
            }
    
            // child class attributes setter
            public Builder model(String model) {
                this.model = model;
                return this;
            }
    
            public Builder screen_size(String screen_size) {
                this.screen_size = screen_size;
                return this;
            }
    
            public Builder warranty_period(String warranty_period) {
                this.warranty_period = warranty_period;
                return this;
            }
    
            @Override
            protected Builder self() {
                return this;
            }
    
            @Override
            ProductDetails build() {
                return new ProductDetails_Computer(this);
            }
    
        }
    
        @Override
        public String toString() {
            return "ProductDetails_Computer [stock= " + stock + ", brand= " + brand + ", model=" + model + ", screen_size="
                    + screen_size + ", warranty_period=" + warranty_period + "]";
        }
    
    }
    

    TestClass

    
    public class TestBuilder {
        public static void main(String[] args) {
            var computerBuilder = new ProductDetails_Computer.Builder(20, "XYZ");
            computerBuilder.brand("Some_brand")
                           // able to invoke child class methods because of recursive generics
                           .screen_size("200").model("some_model");
            
            System.out.println(computerBuilder.build().toString());
            
        }
    }
    

    Output:

    ProductDetails_Computer [stock= 20, brand= Some_brand, model=some_model, screen_size=200, warranty_period=null]
    
    

    You can read about recursive generics here.