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.
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.