You have some class that performs a certain job
public class MyClass {
public MyClass() {
// ...
}
// ...
}
Then, a spec change comes around. The responsibility of the class is the same, but you realize it needs an extra parameter
public MyClass(String someArg)
But you have existing clients that call the no-arg, so you make it call the new constructor with a default parameter value. The init code that used to be in the no-arg is now moved to the constructor with args
public MyClass() {
this(null);
}
Then, a new spec change. The class now has to have ServiceA
to do that
public MyClass(String someArg, ServicA serviceA)
But, again, we have clients that call the old constructors. We move the init logic and provide some default value for the new parameter once again. Unlike someArg
, it's a required dependency so MyClass
will have to come up with some default ServiceA
implementation. Passing null
s will no longer do
public MyClass(String someArg) {
this(someArg, new ServiceAImpl());
}
Some clients don't need to pass someArg
but they still need to pass the required ServiceA
. Those clients will have to pass null
explicitly
MyClass myClass = new MyClass(null, new BellsAndWhistlesServiceA());
I can go on. But the problem is already evident
It's going to snowball into a mess
- The class now manages its defaults. It's a DI responsibility. No class should ever manage its defaults as it in effect scatters configuration all over the codebase making configuration changes difficult
- Ugly, risky calls like this one are now a matter of time
new MyClass("123", new ServiceAImpl2(), null, null, null, new WhateverElse(), null, 123, null)
Builders address that. A buildable class has only one declared dependency – on its builder. Now and forever. You can add optional dependencies through that builder easily. No incompatible changes to the class's API
public class MyClass {
private final String someArg;
// other fields
private MyClass(Builder builder) {
this.someArg = builder.someArg;
// ...
}
public static Builder builder() {
return new Builder();
}
// business logic
public static class Builder {
private String someArg;
public Builder setSomeArg(String someArg) {
this.someArg = someArg;
return this;
}
// other setters
public MyClass build() {
return new MyClass(this);
}
// some instantiating client
MyClass.builder()
.setSomeArg(someArg)
// other setters if necessary
.build();
It's not without drawbacks. It's still unclear how you add required dependencies. You either have to i) add that to the builder constructor and get compilation errors in clients; ii) or add "mandatory" setters and get runtime exceptions (because clients would be calling build()
on builders that are now considered invalid)
And the issue with "distributed" DI is still not resolved. If builders manage their respective class's defaults, is it any better? No, it's not
Still, it may still be a good idea to make all newly created classes buildable by default. Even if they don't (yet) require any inputs at all for their initialization
So what do you think? Should every newly created type be buildable by default (at least, in Java)?