0

I have created a builder for client types A and B.

Client TypeA has two fields fieldA and fieldB (see below), while client TypeB has two additional fields. How can I make sure field C and D are only accessible to client TypeB, i.e. client TypeA should not take field fieldC and fieldD when creating it.

Construction client A:

client.builder()
    .withClientType(ClientType.TypeA)
    .withFieldA(fieldA)   
    .withFieldB(fieldB)
    .build();

Construction client B:

client.builder()
    .withClientType(ClientType.TypeB)
    .withFieldA(fieldA)   
    .withFieldB(fieldB)
    .withFieldC(fieldC)
    .withFieldD(fieldD)
    .build();

What's the correct usage builder pattern in this case?

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • possibly related to https://stackoverflow.com/questions/17839072/dynamic-builder-pattern – Gaurav kumar Singh Oct 26 '22 at 18:41
  • 3
    There should be different builders for both of them, also you can make builders inherit another builder. When you do withTypeA, it should return BuilderA, and when you do withTypeB, it should return BuilderB – RDK Oct 26 '22 at 21:09
  • @RDK, "When you do withTypeA, it should return BuilderA, and when you do withTypeB, it should return BuilderB " how to do this dynamically exactly? Within the `withClientType` method, I can do the following, but it's not recognized. ``` if (type == TypeB) return BuilderForTypeB; else return BuilderForTypeA; ``` – Jackson Oct 26 '22 at 23:20
  • Here is an example for you [EmbeddedChartBuilder|https://developers.google.com/apps-script/reference/spreadsheet/embedded-chart-builder] – RDK Oct 28 '22 at 08:26
  • @Jackson Sorry, there was bug in the Builders of both Clients, fixed it. Now the code should be fine. You can [see it in action online](https://www.jdoodle.com/ia/ygH). – Alexander Ivanchenko Oct 28 '22 at 18:45

2 Answers2

2

It's not possible to construct instances of different classes using the same Builder. As @RDK has pointed out in the comments you have to implement the Builder pattern for each of your classes.

Builder might have the same hierarchy as the classes they are meant to instantiate. That would allow to reuse the functionality of the parent-builder.

Below, I've provided a hierarchy of Builders inspired by the classic implementation from the "Effective Java" by Joshua Bloch (a former Sun Microsystems employee, who led the design and implementation of many Java platform features, including the Java Collections Framework).

Firstly, it's worth noticing that using Builder with inheritance adds a bit of complexity because methods all methods of the Builder should be self-returning, i.e. in order to chain methods each method except build() returns this instance of Builder. But it should a concrete Builder, not it's parent type (because parent might be unaware of all the methods that child has declared).

The answer to this problem would be a self-referencing generic type, in the "Effective Java" a generic type with a recursive type parameter. Don't get intimidated, pause for a second and try to recall how java.lang.Enum looks like. Enum<E extends Enum<E>> - that's an example of the self-reference, recursively pointing at a particular subclass of java.lang.Enum.

Similarly, we need a generic builder referencing to itself ClientBuilder<T extends ClientBuilder<T>> and declaring a self-returning abstract method self() (which return an instance of T) that would be implemented by every concrete class.

Note: there's no need in enum ClientType to differentiate between ClientA and ClientB, since they are already different classes.

That how implementation of an AbstractClient and two concrete client, each having static nested Builder might look like.

Abstract classes AbstractClient + ClientBuilder

public abstract class AbstractClient {
    
    protected ClientType type;
    protected A a;
    protected B b;
    
    protected AbstractClient(ClientBuilder<?> builder) {
        this.a = builder.a;
        this.b = builder.b;
    }
    
    public static abstract class ClientBuilder<T extends ClientBuilder<T>> {
        protected A a;
        protected B b;
        
        public T withFieldA(A a) {
            this.a = a;
            return self();
        }

        public T withFieldB(B b) {
            this.b = b;
            return self();
        }
        // abstract methods
        public abstract AbstractClient build(); // returns a concrete Client
        public abstract T self();               // returns a concrete implementation of ClientBuilder
    }
}

Concrete classes ClientA + ABuilder

public class ClientA extends AbstractClient {
    
    private ClientA(ABuilder builder) {
        super(builder);
    }
    
    // other constructors, getters, etc

    public static ABuilder builder() {
        return new ABuilder();
    }

    public static class ABuilder extends AbstractClient.ClientBuilder<ABuilder> {

        @Override
        public ClientA build() {
            return new ClientA(this);
        }

        @Override
        public ABuilder self() {
            return this; // simply returning This instance of Builder
        }
    }
}

Concrete classes ClientB + BBuilder

public static class ClientB extends AbstractClient {
    
    protected C c;
    protected D d;

    private ClientB(BBuilder builder) {
        super(builder);
        this.c = builder.c;
        this.d = builder.d;
    }
    
    // other constructors, getters, etc
    
    public static BBuilder builder() {
        return new BBuilder();
    }
    
    public static class BBuilder extends AbstractClient.ClientBuilder<BBuilder> {
        protected C c;
        protected D d;

        public BBuilder withFieldC(C c) {
            this.c = c;
            return self();
        }

        public BBuilder withFieldD(D d) {
            this.d = d;
            return self();
        }
        
        @Override
        public ClientB build() {
            return new ClientB(this);
        }
        
        @Override
        public BBuilder self() {
            return this; // simply returning This instance of Builder
        }
    }
}

Usage example (dummy classes A, B, C, D are not exposed, use the link below to see the code in action):

public static void main(String[] args) {
    ClientA clientA = ClientA.builder()
        .withFieldA(new A("1"))
        .withFieldB(new B("Alice"))
        .build();
    
    ClientB clientB = ClientB.builder()
        .withFieldA(new A("2"))
        .withFieldB(new B("Bob"))
        .withFieldC(new C("admin"))
        .withFieldD(new D("fooBar"))
        .build();

    System.out.println(clientA);
    System.out.println(clientB);
}

Output:

ClientA{ a=A[a=1], b=B[b=Alice] }
ClientB{ a=A[a=2], b=B[b=Bob], c=C[c=admin], d=D[d=fooBar] }

A link to Online Demo.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
0

Another example that I like. The idea here is, there are several kind of "Root" [A,B,C], each with their own attributes. At any point you can switch to a different Builder which gives you a different kind of "Root" with common behaviours stored in AbstractBuilder.

It is also possible that they inherit each other and similarly their builders can also inherit each other.

abstract class AbstractBuilder {
    protected String common;
    
    public AbstractBuilder withCommon(String common){
        this.common = common;
        return this;
    }

    public abstract Root build();
    public A.BuilderA asBuilderA(){
        return new A.BuilderA(common);
    }

    public B.BuilderB asBuilderB(){
        return new B.BuilderB(common);
    }

    public C.BuilderC asBuilderC(){
        return new C.BuilderC(common);
    }
}
abstract class Root{
    private final String common;

    @Override
    public String toString() {
        return "Root{" +
                "common='" + common + '\'' +
                '}';
    }

    Root(String common) {
        this.common = common;
    }
}
class A extends Root {
    private final String a;

    @Override
    public String toString() {
        return "A{" +
                "a='" + a + '\'' +
                "} " + super.toString();
    }

    public A(String common, String a) {
        super(common);
        this.a = a;
    }
    public static class BuilderA extends AbstractBuilder {
        protected String a;

        public BuilderA(String common) {
            super();
            withCommon(common);
        }

        public BuilderA withA(String a){
            this.a = a;
            return this;
        }

        public A build(){
            return new A(common, a);
        }
    }
}

class B  extends Root {
    private final String b;

    @Override
    public String toString() {
        return "B{" +
                "b='" + b + '\'' +
                "} " + super.toString();
    }

    public B(String common, String b) {
        super(common);
        this.b = b;
    }
    public static class BuilderB extends AbstractBuilder {
        protected String b;
        
        public BuilderB(String common) {
            super();
            withCommon(common);
        }
        public BuilderB withB(String b){
            this.b = b;
            return this;
        }

        public B build(){
            return new B(common, b);
        }
    }
}

class C extends Root {
    private final String c;

    @Override
    public String toString() {
        return "C{" +
                "c='" + c + '\'' +
                "} " + super.toString();
    }

    public C(String common, String c) {
        super(common);
        this.c = c;
    }
    public static class BuilderC extends AbstractBuilder {
        protected String c;

        public BuilderC(String common) {
            super();
            withCommon(common);
        }
        public BuilderC withC(String c){
            this.c = c;
            return this;
        }

        public C build(){
            return new C(common, c);
        }
    }
}

public class BuilderExample {

    public static void main(String[] args) {
        A.BuilderA builderA = new A.BuilderA("common").withA("a")
                .asBuilderB().withB("b")
                .asBuilderC().withC("c")
                .asBuilderA();
        System.out.println(builderA.build()); //A{a='null'} Root{common='common'}
        System.out.println(builderA.withA("aa").build()); //A{a='aa'} Root{common='common'}
        System.out.println(builderA.asBuilderB().withB("bb").build()); //B{b='bb'} Root{common='common'}
    }
}
RDK
  • 144
  • 1
  • 1
  • 9