Let's look at the options, where we can place the validation code:
- Inside the setters in builder.
- Inside the
build()
method.
- Inside the constructed entity: it will be invoked in
build()
method when the entity is being created.
Option 1 allows us to detect problems earlier, but there can be complicated cases when we can validate input only having the full context, thus, doing at least part of validation in build()
method. Thus, choosing option 1 will lead to inconsistent code with part of validation being done in one place and another part being done in other place.
Option 2 isn't significantly worse than option 1, because, usually, setters in builder are invoked right before the build()
, especially, in fluent interfaces. Thus, it's still possible to detect a problem early enough in most cases. However, if the builder is not the only way to create an object, it will lead to duplication of validation code, because you'll need to have it everywhere where you create an object. The most logical solution in this case will be to put validation as close to created object as possible, that is, inside of it. And this is the option 3.
From SOLID point of view, putting validation in builder also violates SRP: the builder class already has responsibility of aggregating the data to construct an object. Validation is establishing contracts on its own internal state, it's a new responsibility to check the state of another object.
Thus, from my point of view, not only it's better to fail late from design perspective, but it's also better to fail inside the constructed entity, rather than in builder itself.
UPD: this comment reminded me of one more possibility, when validation inside the builder (option 1 or 2) makes sense. It does make sense if the builder has its own contracts on the objects it is creating. For example, assume that we have a builder that constructs a string with specific content, say, list of number ranges 1-2,3-4,5-6
. This builder may have a method like addRange(int min, int max)
. The resulting string does not know anything about these numbers, neither it should have to know. The builder itself defines the format of the string and constraints on the numbers. Thus, the method addRange(int,int)
must validate the input numbers and throw an exception if max is less than min.
That said, the general rule will be to validate only the contracts defined by the builder itself.
Builders are most useful when your object needs a lot of arguments/dependencies to be useful, or you want to allow many different ways of constructing the object.
Off the top of my head, I can imagine someone might want to "build" objects in a 3D game like this:
// Just ignore the fact that this hypothetical god class is coupled to everything ever
new ObjectBuilder(x, y, z).importBlenderMesh("./meshes/foo")
.syncWithOtherPlayers(serverIP)
.compileShaders("./shaders/foo.vert", "./shaders/foo.frag")
.makeDestructibleRigidBody(health, weight)
...
I would argue this example is more readable with the builder methods I made up just now than it would be with optional parameters:
new Object(x, y, z, meshType: MESH.BLENDER,
meshPath: "./meshes/foo",
serverToSyncWith: serverIP,
vertexShader: "./shaders/foo.vert",
physicsType: PHYSICS_ENGINE.RIGID_DESTRUCTIBLE,
health: health,
weight: weight)
...
In particular, the information implied by the builder method names has to get replaced by yet more parameters, and it's much easier to forget about one parameter in a group of closely related parameters. In fact, the fragment shader is missing, but you wouldn't notice that unless you knew to look for it.
Of course, if your object only takes one to five arguments to construct, there's no need to get the builder pattern involved, whether or not you have named/optional parameters.
Best Answer
It boils down to never having a Foo in a half constructed state
Some reasons that spring to mind:
The builder's is by nature tied to the class it is constructing. You can think of the intermediate states of the builder as partial application of the constructor, so your argument that it is too coupled isn't persuasive.
By passing the whole builder in, fields in MyClass can be final, making reasoning about MyClass easier. If the fields are not final, you could just as easily have fluent setters than a builder.