1

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.

Jason Rich Darmawan
  • 1,607
  • 3
  • 14
  • 31
  • Maybe this [thread](https://stackoverflow.com/questions/21086417/builder-pattern-and-inheritance) helps? – Ralf May 22 '21 at 09:37

1 Answers1

0

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.

adarsh
  • 1,393
  • 1
  • 8
  • 16
  • Is there are reason to implement recursive generic instead of directly using the `extends ProductDetails.Builder` to the `public static class ProductDetails_ComputerBuilder`? – Jason Rich Darmawan May 22 '21 at 12:37
  • @kidfrom Recursive generics is `Builder>`. As of `public static class ProductDetails_ComputerBuilder extends ProductDetails.Builder` try and see the problems you face. :) Even if you find a workaround, you should know that builder pattern intends to [separate construction from representation](https://en.wikipedia.org/wiki/Builder_pattern#:~:text=a%20complex%20object.-,Definition,process%20can%20create%20different%20representations.). – adarsh May 22 '21 at 14:28
  • I genuinely confused. I use JUnit to test each attributes of the `ProductDetails`, `ProductDetails_Computer`, `ProductDetails_Food` objects and they pass the tests without recursive generics. Please kindly explain why you recommend the use of recursive generics and I will mark it as answer. – Jason Rich Darmawan May 22 '21 at 14:45
  • @kidfrom Is that because you called `computerBuilder.screen_size("200").model("some_model").brand("Some_brand")` in this order (`brand()` at last) ? Your `brand()` method implementation must have a return type `ProductDetails.Builder`. This is where recursive generics comes in: You're not forced to chain your child class methods before parent class methods. – adarsh May 22 '21 at 15:25
  • @kidfrom If above is not the case, you should add your implementation as an answer. That will help everyone :) – adarsh May 22 '21 at 15:40
  • I tested your code and I still need type casting if I force the child method to go first e.g `(ProductDetails_Computer) new ProductDetails_Computer.Builder(1, "Quebec, Canada").model("Macbook Air M1").brand("Apple").build();`, mine also face the same issue. – Jason Rich Darmawan May 22 '21 at 15:57
  • I will try to use generic now to remove the need of type casting. – Jason Rich Darmawan May 22 '21 at 15:58
  • @kidfrom Error doesn't come when you chain child method first. Compilation error (in your implementation) will come when you call parent method first and then child methods. But with recursive generics, this won't be the case. You can chain methods in any order. You won't see any casting issue with my answer. You're missing something. Copy my answer as-is and compare with your implementation – adarsh May 22 '21 at 16:13
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232735/discussion-between-adarsh-and-kidfrom). – adarsh May 22 '21 at 16:16
  • Adarsh, are you still there? I waited for about 30 minutes. Maybe we can discuss later about why recursive generic is overkill? – Jason Rich Darmawan May 22 '21 at 17:07