The correct approach would be to have one builder per class. I have seen two different builder implementations, let's call them lazy and eager (maybe one of them is not a strict builder, but both of them actually build instances).
Here are lazy builders for Car
s and Truck
s:
public abstract class AbstractLazyCarBuilder<T extends Car, B extends AbstractLazyCarBuilder<T, B>> {
private String brand;
private String speed;
public B brand(String brand) {
this.brand = brand;
return (B) this;
}
public B speed(String speed) {
this.speed = speed;
return (B) this;
}
public T build() {
T car = this.create();
this.fill(car);
return car;
}
protected abstract T create();
protected void fill(T car) {
car.setBrand(this.brand);
car.setSpeed(this.speed);
}
}
public class LazyCarBuilder extends AbstractLazyCarBuilder<Car, LazyCarBuilder> {
@Override
protected Car create() {
return new Car();
}
}
public class LazyTruckBuilder extends AbstractLazyCarBuilder<Truck, LazyTruckBuilder> {
private String length;
public LazyTruckBuilder length(String length) {
this.length = length;
return this;
}
@Override
protected Truck create() {
return new Truck();
}
@Override
protected void fill(Truck truck) {
super.fill(truck); // very important! fills truck with car's common attributes
truck.setLength(this.length);
}
}
Usage:
Truck truck = new LazyTruckBuilder().brand("ford").speed("40").length("30").build();
It might not be the standard builder implementation. It has two generic parameters: the type of the object being built and the type of the builder itself. This last one is to avoid casting the returned object when using the builder methods.
It has a create()
method that returns the empty specific instance (could have also been created by reflection) and a fill()
method that sets all the attributes to the created object. This builder is lazy in the sense that the object is created and initialized when you call build()
on the builder.
The eager version of this builder should use reflection to create the object being built:
public abstract class AbstractEagerCarBuilder<T extends Car, B extends AbstractEagerCarBuilder<T, B>> {
protected final T instance; // needs to be seen by subclasses
protected AbstractEagerCarBuilder() {
try {
// Reflection magic to get type of specific car
ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
Class<T> clazz = (Class<T>) type.getActualTypeArguments()[0];
// Create the specific car by reflection
this.instance = clazz.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Could not create specific instance", e);
}
}
public B brand(String brand) {
this.instance.setBrand(brand);
return (B) this;
}
public B speed(String speed) {
this.instance.setSpeed(speed);
return (B) this;
}
public T build() {
return this.instance;
}
}
public class EagerCarBuilder extends AbstractEagerCarBuilder<Car, EagerCarBuilder> {
// empty: just pass generic parameters
}
public class EagerTruckBuilder extends AbstractEagerCarBuilder<Truck, EagerTruckBuilder> {
private String length;
public EagerTruckBuilder length(String length) {
this.instance.setLength = length;
return this;
}
}
Usage:
Truck truck = new EagerTruckBuilder().brand("gmc").speed("45").length("32").build();
Here, the truck instance is actually created at builder's creation time. Then, builder methods fill the eagerly created instance with attributes, one by one.
Whether to use one or the other, is up to you. Plase let me know if this has errors, as I couldn't test it, as well as if you have any questions (I'm so used to this kind of code that I might be lacking some useful explanation).