How to subclass a Java builder class?

741 views Asked by At

I have two builders - PayloadA and PayloadB. To make example simpler, I have removed lot of other fields.

  • PayloadA.Builder constructor takes processName, genericRecord as an input parameter and then extract few things from genericRecord. And on that I am doing validation.
  • PayloadB.Builder constructor also takes processName, genericRecord as an input parameter and then it extract few different things from genericRecord as compared to above. And on those different fields I am doing validation.

As you can see, common thing between those two Payload?.Builder is processName, genericRecord, extracting oldTimestamp value and then isValid method.

Below is my PayloadA class:

public final class PayloadA {
  private final String clientId;
  private final String deviceId;
  private final String processName;
  private final GenericRecord genericRecord;
  private final Long oldTimestamp;

  private PayloadA(Builder builder) {
    this.clientId = builder.clientId;
    this.deviceId = builder.deviceId;
    this.processName = builder.processName;
    this.genericRecord = builder.genericRecord;
    this.oldTimestamp = builder.oldTimestamp;
  }

  public static class Builder {
    private final String processName;
    private final GenericRecord genericRecord;
    private final String clientId;
    private final String deviceId;
    private final Long oldTimestamp;

    public Builder(PayloadA payload) {
      this.processName = payload.processName;
      this.genericRecord = payload.genericRecord;
      this.clientId = payload.clientId;
      this.deviceId = payload.deviceId;
      this.oldTimestamp = payload.oldTimestamp;
    }

    public Builder(String processName, GenericRecord genericRecord) {
      this.processName = processName;
      this.genericRecord = genericRecord;
      this.clientId = (String) DataUtils.parse(genericRecord, "clientId");
      this.deviceId = (String) DataUtils.parse(genericRecord, "deviceId");
      this.oldTimestamp = (Long) DataUtils.parse(genericRecord, "oldTimestamp");
    }

    // calling this method to validate
    public boolean isValid() {
      return isValidClientIdDeviceId();
    }

    private boolean isValidClientIdDeviceId() {
        // validate here
    }

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

  // getter here
}

Below is my PayloadB class:

public final class PayloadB {
  private final GenericRecord genericRecord;
  private final String processName;
  private final String type;
  private final String datumId;
  private final Long oldTimestamp;

  private PayloadB(Builder builder) {
    this.processName = builder.processName;
    this.genericRecord = builder.genericRecord;
    this.type = builder.type;
    this.datumId = builder.datumId;
    this.oldTimestamp = builder.oldTimestamp;
  }

  public static class Builder {
    private final GenericRecord genericRecord;
    private final String processName;
    private final String type;
    private final String datumId;
    private final Long oldTimestamp;

    public Builder(PayloadB payload) {
      this.processName = payload.processName;
      this.genericRecord = payload.genericRecord;
      this.type = payload.type;
      this.datumId = payload.datumId;
      this.oldTimestamp = payload.oldTimestamp;
    }

    public Builder(String processName, GenericRecord genericRecord) {
      this.processName = processName;
      this.genericRecord = genericRecord;
      this.type = (String) DataUtils.parse(genericRecord, "type");
      this.datumId = (String) DataUtils.parse(genericRecord, "datumId");
      this.oldTimestamp = (Long) DataUtils.parse(genericRecord, "oldTimestamp");
    }

    // calling this method to validate
    public boolean isValid() {
      return isValidType() && isValidDatumId();
    }

    private boolean isValidType() {
        // validate here
    }

    private boolean isValidDatumId() {
        // validate here
    }

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

    // getter here

}

Now is there any way I can use concept of abstract class here? I can create an abstract class Payload but what should be the stuff inside my abstract class:

public final class PayloadA extends Payload { ... }
public final class PayloadB extends Payload { ... }

And then once I build both my builder, I will pass it to some other method and there I want to access all the fields using getters. So let's say I have build PayloadA so I will send to execute method as shown below and then in that method, I want to extract all the fields of PayloadA. Similarly if I send PayloadB to execute method, then I want to extract all the fields of PayloadB class using getters. How can I do this?

private void execute(Payload payload) {

    // How can I access fields of PayloadA or PayloadB 
    // depending on what was passe
}
2

There are 2 answers

2
Siegfried Weber On BEST ANSWER

Create a super class for the payloads only if the mentioned fields are not common by a coincidence. You can move common fields and methods (but not the builders) in there. You could even create a super class for the builders but it will probably clutter the code too much.

If you really have a use for the payload super class then you can implement your execute method with the Visitor Pattern:

First, you have to create a visitor where you can access your concrete classes:

public class PayloadVisitor {

    public void visit(PayloadA payloadA) {
        // use payload A here
    }

    public void visit(PayloadB payloadB) {
        // use payload B here
    }
}

Then you have to add a method to your super class accepting the visitor:

public abstract class Payload {

    // common fields and methods

    public abstract void accept(PayloadVisitor visitor);
}

Override the method accept in the subclasses:

public final class PayloadA extends Payload {

    // ...

    @Override
    public void accept(PayloadVisitor visitor) {
        visitor.visit(this);
    }
}

public final class PayloadB extends Payload {

    // ...

    @Override
    public void accept(PayloadVisitor visitor) {
        visitor.visit(this);
    }
}

Your method execute just redirects the call to the according visit method:

private void execute(Payload payload) {
    payload.accept(new PayloadVisitor());
}

The visitor pattern can be overwhelming. You can also keep it simple and use instanceof to determine the concrete class.

0
ddarellis On

I think the question here is if PayloadA and PayloadB are sharing something meaning full together for the design. If the logic is somehow the same except one parameter then you can have one class.

Maybe you can have the abstract class, and for the implementation for a specific field you can return your concrete value for a specific implementation.

For example Abstract class has abstract setter/getter for a field and when you implement that method to PayloadA and PayloadB you can return the field you want.

I think the problem is the design here not how to do it. See what your classes really are and then you have many options