Java: How to implement a step builder for which the order of setters doesn’t matter

builder-patterndesign-patterns

Edit: I'd like to point out that this question describes a theoretical problem, and I am aware that I can use constructor arguments for mandatory parameters, or throw a runtime exception if the API is used incorrectly. However, I am looking for a solution that does not require constructor arguments or runtime checking.

Imagine you have a Car interface like this:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

As the comments would suggest, a Car must have an Engine and Transmission but a Stereo is optional. That means a Builder that can build() a Car instance should only ever have a build() method if an Engine and Transmission have both already been given to the builder instance. That way the type checker will refuse to compile any code that attempts to create a Car instance without an Engine or Transmission.

This calls for a Step Builder. Typically you'd implement something like this:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

There's a chain of different builder classes (Builder, BuilderWithEngine, CompleteBuilder), that add one required setter method after another, with the last class containing all optional setter methods as well.
This means that users of this step builder are confined to the order in which the author made mandatory setters available. Here is an example of possible uses (note that they are all strictly ordered: engine(e) first, followed by transmission(t), and finally the optional stereo(s)).

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

However, there are plenty of scenarios in which this is not ideal for the builder's user, especially if the builder has not just setters, but also adders, or if the user cannot control the order in which certain properties for the builder will become available.

The only solution I could think of for that is very convoluted: For every combination of mandatory properties having been set or having not yet been set, I created a dedicated builder class that knows which potential other mandatory setters need to be called before arriving at a state where the build() method should available, and each one of those setters returns a more complete type of builder which is one step closer to containing a build() method.
I added the code below, but you might say that I'm using the type system to create a FSM that lets you create a Builder, which can be turned into either a BuilderWithEngine or BuilderWithTransmission, which both can then be turned into a CompleteBuilder, which implements the build() method. Optional setters can be invoked on any of these builder instances.
enter image description here

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

As you can tell, this doesn't scale well, as the number of different builder classes required would be O(2^n) where n is the number of mandatory setters.

Hence my question: Can this be done more elegantly?

(I'm looking for an answer that works with Java, although Scala would be acceptable too)

Best Answer

You seem to have two different requirements, based on the method calls you provided.

  1. Only one (required) engine, only one (required) transmission, and only one (optional) stereo.
  2. One or more (required) engines, one or more (required) transmissions, and one or more (optional) stereos.

I think the first issue here is that you don't know what you want the class to do. Part of that is that it's not known what you want the built object to look like.

A car can only have one engine and one transmission. Even hybrid cars only have one engine (perhaps a GasAndElectricEngine)

I'll address both implementations:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

and

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

If an engine and transmission are required, then they should be in the constructor.

If you don't know what engine or transmission is required, then don't set one yet; it's a sign that you're creating the builder too far up the stack.

Related Topic