I’m trying to implement a sort-of Builder pattern that takes in raw data, and depending on what needs to be built, gets a predefined schema with a set of Consumers, specifically targeted for what needs to be built. The base class Builder handles all the logic of deciding what consumers on the Schema need to be called (with the accept method of Consumer). Sub classes of Builder implement their own methods (on top of the ‘standard’ ones available in Builder) and have their own Schema defined. The custom Schemas with their sets of available Consumers are how Builder builds what needs to be built from the raw data. Here’s the simplified code with Schema’s just having one Consumer ‘reference’ to the custom builders. I’m having issues with the combination of inheritance, typing, and generics. The BiConsumer.accept()
method in Builder.build()
does not like how it’s being called. I can’f figure out how to fix the generic typing/identifiers/wild-cards issues shown by this error:
“The method accept(capture#4-of ? extends Builder, V) in the type BiConsumer<capture#4-of ? extends Builder,V> is not applicable for the arguments (Builder, V)”
I’ve played with wild-cards, identifiers, etc and I’ve read quite a bit, but I can’t piece the solution together. I ‘fix’ it in one place, it breaks in another. I feel like I’m missing some knowledge (of a generics feature perhaps) and understanding of how generic types work with inheritance.
Here’s the simplified code that shows the issue with BiConsumer.accept(), the error I mentioned above.
Main code with logic to process raw data and set the properties of the object it’s building. The object must be Vehicle or a sub class of Vehicle. The ’empty’ object is given to the Builder.build() and filled using the Consumers defined on Schema. To simplify, mock raw data is hard-coded here.
public class Builder<V extends Vehicle> {
String rawDataToProcess = "4,green";
Schema<? extends Builder<V>, V> schema;
public Builder(Schema<? extends Builder<V>, V> schema) {
this.schema = schema;
}
public void build(V vehicle) {
schema.methodReference.accept(this, vehicle);
}
public void processColor(V vehicle) {
// Use raw data to process to get color and set it on vehicle.
System.out.println(vehicle.color);
}
}
CarBuilder builds Cars (extends Vehicle). We should only need to add methods that process the new properties that Car has and Vehicle doesn’t. The Schema for the Car object will provide access to these methods through Consumers.
public class CarBuilder extends Builder<Car> {
public CarBuilder(Schema<CarBuilder, Car> schema) {
super(schema);
}
public void processNumWheels(Car car) {
String numWheels = rawDataToProcess.split(",")[0];
car.numWheels = Integer.valueOf(numWheels);
System.out.println(car.numWheels);
}
}
Schema has predefined schema’s for Builder to build Vehicle and CarBuilder to build Car. To simplify here, each schema ‘exposes’ only one Consumer.
import java.util.function.BiConsumer;
public class Schema<B extends Builder<V>, V extends Vehicle> {
BiConsumer<? extends Builder<V>, V> methodReference;
public static Schema<Builder<Vehicle>, Vehicle> builderSchema = new Schema<>(Builder::processColor);
public static Schema<CarBuilder, Car> carBuilderSchema = new Schema<>(CarBuilder::processNumWheels);
private Schema(BiConsumer<B, V> methodReference) {
this.methodReference = methodReference;
}
}
Sandbox, which just has a main() to test things.
public class Sandbox {
public static void main(String[] args) {
Schema<CarBuilder, Car> schema = Schema.carBuilderSchema;
CarBuilder cb = new CarBuilder(schema);
Car c = new Car();
cb.build(c);
}
}