I agree with your view that a Builder is really just a glorified constructor, and that the "builder pattern is just a way to build an object similar to what a constructor does".
However, here are a few of scenarios where the complexity of constructing an object makes the use of a Builder compelling.
Object's dependencies collected over period of time
In Java, StringBuilder is commonly used when building a string over a period of time, or rather, within a complex procedure. For instance, if a server is communicating with a client over a socket, and wants to append some client responses to the string, but not others, and perhaps remove certain responses that were previously appended,the StringBuilder class can be used to do so. At the end of the client/server session, the server can invoke StringBuilder#toString to get the built String.
Lots of parameters
If a constructor has dozens of parameters, it may make the code more readable or easy to maintain to use a builder.
E.g.
new Foo(1,2,3,4,5,6,7,8,9,10,11,12)
Vs.
Foo.newBuilder()
.bar(1)
.bar(2)
.quux(3)
...
.build()
Constructing object graphs
Similar to the "lots of parameters" scenario, I think that the scenario where a builder is most compelling is when constructing a complex object graph. The other answers in this question refer to the telescoping anti-pattern. This scenario (building a complex object graph) can lead to "telescoping", which the Builder helps resolve.
For instance, imagine you have an object-oriented pipeline interface, where a Pipeline depends on Sequence which depends on Stage. A PipelineBuilder would not only provide a nice wrapper around the constructor of Pipeline, but also around the constructors Sequence and Stage, allowing you to compose a complex Pipeline from a single Builder interface.
Instead of telescoping constructors like so:
new Pipeline(
new Sequence(
new Stage(
new StageFunction() {
public function execute() {...}
}
),
new Stage(
new StageFunction() {
public function execute() {...}
}
)
)
)
A PipelineBuilder would allow you to "collapse" the telescope.
Pipeline.newBuilder()
.sequence()
.stage(new StageFunction () {
public function execute() {...}
})
.stage(new StageFunction () {
public function execute() {...}
})
.build()
(Even though I have used indentation in a way that is reflective of the telescoping constructors, this is merely cosmetic, as opposed to structural.)
Answer from maxenglander on Stack OverflowI agree with your view that a Builder is really just a glorified constructor, and that the "builder pattern is just a way to build an object similar to what a constructor does".
However, here are a few of scenarios where the complexity of constructing an object makes the use of a Builder compelling.
Object's dependencies collected over period of time
In Java, StringBuilder is commonly used when building a string over a period of time, or rather, within a complex procedure. For instance, if a server is communicating with a client over a socket, and wants to append some client responses to the string, but not others, and perhaps remove certain responses that were previously appended,the StringBuilder class can be used to do so. At the end of the client/server session, the server can invoke StringBuilder#toString to get the built String.
Lots of parameters
If a constructor has dozens of parameters, it may make the code more readable or easy to maintain to use a builder.
E.g.
new Foo(1,2,3,4,5,6,7,8,9,10,11,12)
Vs.
Foo.newBuilder()
.bar(1)
.bar(2)
.quux(3)
...
.build()
Constructing object graphs
Similar to the "lots of parameters" scenario, I think that the scenario where a builder is most compelling is when constructing a complex object graph. The other answers in this question refer to the telescoping anti-pattern. This scenario (building a complex object graph) can lead to "telescoping", which the Builder helps resolve.
For instance, imagine you have an object-oriented pipeline interface, where a Pipeline depends on Sequence which depends on Stage. A PipelineBuilder would not only provide a nice wrapper around the constructor of Pipeline, but also around the constructors Sequence and Stage, allowing you to compose a complex Pipeline from a single Builder interface.
Instead of telescoping constructors like so:
new Pipeline(
new Sequence(
new Stage(
new StageFunction() {
public function execute() {...}
}
),
new Stage(
new StageFunction() {
public function execute() {...}
}
)
)
)
A PipelineBuilder would allow you to "collapse" the telescope.
Pipeline.newBuilder()
.sequence()
.stage(new StageFunction () {
public function execute() {...}
})
.stage(new StageFunction () {
public function execute() {...}
})
.build()
(Even though I have used indentation in a way that is reflective of the telescoping constructors, this is merely cosmetic, as opposed to structural.)
From the Wikipedia page:
The telescoping constructor anti-pattern occurs when the increase of object constructor parameter combination leads to an exponential list of constructors. Instead of using numerous constructors, the builder pattern uses another object, a builder, that receives each initialization parameter step by step and then returns the resulting constructed object at once
So if I have an object requiring many construction parameters, and those parameters are required in a variety of combinations (thus making some parameters optional) then a builder is a good approach.
e.g. I could create multiple different constructors for an object, or I could do the following:
new ObjectBuilder().withParam1(1).withParam4(4).withParam19(19).build();
thus allowing me to select the parameters required, and not have to define many different constructors. Note also that the above can allow you to populate a builder, and set parameters/call build() multiple times to create a set of related objects easily.
Constructor with tons of parameters vs builder pattern - Software Engineering Stack Exchange
java - lombok @Builder vs constructor - Stack Overflow
Is it better to use setters over constructors when a class have too many attributes?
java - Builder with constructor or factory method? - Software Engineering Stack Exchange
What is the difference between a Builder and a constructor?
What does @builder do in Java?
What are the 3 types of constructor?
Videos
The Builder Pattern does not solve the “problem” of many arguments. But why are many arguments problematic?
- They indicate your class might be doing too much. However, there are many types that legitimately contain many members that cannot be sensibly grouped.
- Testing and understanding a function with many inputs gets exponentially more complicated – literally!
- When the language does not offer named parameters, a function call is not self-documenting. Reading a function call with many arguments is quite difficult because you have no idea what the 7th parameter is supposed to do. You wouldn't even notice if the 5th and 6th argument were swapped accidentally, especially if you're in a dynamically typed language or everything happens to be a string, or when the last parameter is
truefor some reason.
Faking named parameters
The Builder Pattern addresses only one of these problems, namely the maintainability concerns of function calls with many arguments∗. So a function call like
MyClass o = new MyClass(a, b, c, d, e, f, g);
might become
MyClass o = MyClass.builder()
.a(a).b(b).c(c).d(d).e(e).f(f).g(g)
.build();
∗ The Builder pattern was originally intended as a representation-agnostic approach to assemble composite objects, which is a far greater aspiration than just named arguments for parameters. In particular, the builder pattern does not require a fluent interface.
This offers a bit of extra safety since it will blow up if you invoke a builder method that doesn't exist, but it otherwise does not bring you anything that a comment in the constructor call wouldn't have. Also, manually creating a builder requires code, and more code can always contain more bugs.
In languages where it is easy to define a new value type, I've found that it's way better to use microtyping/tiny types to simulate named arguments. It is named so because the types are really small, but you end up typing a lot more ;-)
MyClass o = new MyClass(
new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
new MyClass.G(g));
Obviously, the type names A, B, C, … should be self-documenting names that illustrate the meaning of the the parameter, often the same name as you'd give the parameter variable. Compared with the builder-for-named-arguments idiom, the required implementation is a lot simpler, and thus less likely to contain bugs. For example (with Java-ish syntax):
class MyClass {
...
public static class A {
public final int value;
public A(int a) { value = a; }
}
...
}
The compiler helps you guarantee that all arguments were provided; with a Builder you'd have to manually check for missing arguments, or encode a state machine into the host language type system – both would likely contain bugs.
There is another common approach to simulate named arguments: a single abstract parameter object that uses an inline class syntax to initialize all fields. In Java:
MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});
class MyClass {
...
public static abstract class Arguments {
public int argA;
public String ArgB;
...
}
}
However, it is possible to forget fields, and this is a quite language-specific solution (I've seen uses in JavaScript, C#, and C).
Fortunately, the constructor can still validate all arguments, which is not the case when your objects are created in a partially-constructed state, and require the user to provide further arguments via setters or an init() method – those require the least coding effort, but make it more difficult to write correct programs.
So while there are many approaches to address the “many unnamed parameters make code difficult to maintain problem”, other problems remain.
Approaching the root problem
For example the testability problem. When I write unit tests, I need the ability to inject test data, and to provide test implementations to mock out dependencies and operations that have external side effects. I can't do that when you instantiate any classes within your constructor. Unless the responsibility of your class is the creation of other objects, it shouldn't instantiate any non-trivial classes. This goes hand in hand with the single responsibility problem. The more focussed the responsibility of a class, the easier it is to test (and often easier to use).
The easiest and often best approach is for the constructor to take fully-constructed dependencies as parameter, though this shoves the responsibility of managing dependencies to the caller – not ideal either, unless the dependencies are independent entities in your domain model.
Sometimes (abstract) factories or full dependency injection frameworks are used instead, though these might be overkill in the majority of use cases. In particular, these only reduce the number of arguments if many of these arguments are quasi-global objects or configuration values that don't change between object instantiation. E.g. if parameters a and d were global-ish, we'd get
Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);
class MyClass {
MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
this.depA = deps.newDepA(b, c);
this.depB = deps.newDepB(e, f);
this.g = g;
}
...
}
class Dependencies {
private A a;
private D d;
public Dependencies(A a, D d) { this.a = a; this.d = d; }
public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
public MyClass newMyClass(B b, C c, E e, F f, G g) {
return new MyClass(deps, b, c, e, f, g);
}
}
Depending on the application, this might be a game-changer where the factory methods end up having nearly no arguments because all can be provided by the dependency manager, or it might be a large amount of code that complicates instantiation for no apparent benefit. Such factories are way more useful for mapping interfaces to concrete types than they are for managing parameters. However, this approach tries to addresses the root problem of too many parameters rather than just hiding it with a pretty fluent interface.
Builder pattern does not solve anything for you and does not fix design failures.
If you have a class needing 10 parameters to be constructed, making a builder to construct it will not suddenly make your design better. You should opt for refactoring the class in question.
On the other hand, if you have a class, perhaps a simple DTO, where certain attributes of the class are optional, builder pattern could ease the construction of said object.
Consider:
Order order = new Order("Alan", "Smith", 2, 6, "Susan", "Smith");
What do the parameters mean? We have to look at the constructor spec to find out.
Now with a builder:
Order order = Order.builder()
.originatorFirstName("Alan")
.originatorLastName("Smith")
.lineItemNumber(2)
.quantity(6)
.recipientFirstName("Susan")
.recipientLastName("Smith")
.build();
It's more wordy, but it's very clear to read, and with IDE assistance it's easy to write too. The builders themselves are a bit of a chore to write, but code-generation tools like Lombok help with that.
Some people argue that if your code needs builders to be readable, that's exposing other smells. You're using too many basic types; you're putting too many fields in one class. For example, consider:
Order order = new Order(
new Originator("Alan", "Smith"),
new ItemSet(new Item(2), 6),
new Recipient("Susan", "Smith"));
... which is self-explanatory without using a builder, because we are using more classes with single-responsibilities and fewer fields.
This is not a lombok specific feature, this is called a builder pattern.
Imagine you have a class with 20 parameters, and 10 of them are optional. You could somehow make tons of constructors taking care of this logic, or make a constructor with all those arguments and pass nulls in some places. Isn't builder pattern simply easier?
Sometimes I get lost when a class have to many attributes and I dont know the order of the attibutes, is it a better standard to use setters instead?