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.
null
object when there was a problem inbuild()
.