1

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 nulls 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

  1. 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
  2. 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)?

13
  • You may say, "If you got a DI layer set up alright, the only clients that are going break with new required dependencies are you DI classes, no big deal", and you'll probably be right. The thing is the DI layer is only emerging in our app Commented Sep 7 at 15:30
  • Introduce monads for the optional stuff? Commented Sep 7 at 15:34
  • @πάνταῥεῖ never heard of "monads" Commented Sep 7 at 15:37
  • It looks like your clients should use interfaces instead of concrete classes. And then you can provide several different implementations, hidden behind builders/factories (note the "s") that return the same interface. Concrete class becomes then an implementation detail, user does not even have to be aware of its existence.
    – freakish
    Commented Sep 7 at 15:46
  • 2
    @SergeyZolotarev I understand. What I'm saying is: don't let them. Not directly anyway. Make a backwards incompatible change. I'm telling you: this will profit you in the future. Unless for some reason you can't. But then there's not much you can do, except for adding optional parameters/new constructors.
    – freakish
    Commented Sep 7 at 15:51

3 Answers 3

5

This might be an XY problem. Making things "buildable" isn't the issue. All those optional constructor parameters are the real problem. Lumping a builder on top of this mess only adds a layer of frosting on top of a mud cake.


I'm always weary of optional dependencies in a class constructor. I like constructors that do two things:

  1. Declare the minimal dependencies necessary to use the object.
  2. Ensure the object is fully usable.

Optional dependencies in a constructor make me wonder why an argument is needed. When do I need this? Now? Later? Can I pass null now? What if I call a method later and the null suddenly becomes a problem? How will I debug the failure? Is there a default implementation, and if so, what does it do? What side effects does it have? So many things run through my head.

I would also question why MyClass is being initialized all over the place instead of being registered in a DI container and passed around like every other common dependency. This might not be solvable, depending on the structure of the existing code base.

I see a number of ways forward. I'm not sure if they will fix the design of this class, but it should improve the design enough so you can see the next improvement that can be made.

  • Identify the methods where the optional dependency is used. If it is not used prolifically, consider making this a method parameter instead of a constructor parameter. This won't work if you need to guarantee the dependency is the same object upon each method invocation.
  • Utilize the Null Object Pattern as the default implementation for optional dependencies, and document this in comments for the constructor.
  • Design the class for inheritance so a concrete sub type declares the additional dependencies, which will be required. This allows for selective specialization of MyClass without impacting existing clients. Be mindful of the Liskov Substitution Principle so passing a sub type to a method requiring MyClass doesn't result in unexpected behavior.
  • Consider splitting MyClass into completely separate classes with no inheritance relationship to one another. Perhaps the use of this class has diverged enough that copying and pasting code will result in fewer maintenance headaches simply because these things can evolve on their own without affecting each other.
    • Common logic can be moved into other classes that are shared between the two new classes. Code reuse is the issue to solve, in this case.
    • You can introduce interfaces for methods that do not initialize these classes, but still need to use them provided the interface is cohesive with all the sub types.

The one thing I strongly caution against is using a "builder" as described in the question. A builder isn't a builder because of what you name it. It is a builder because of how it is used, and the code example is not using it as a builder. What you describe in the question is a Service Locator, a known anti-pattern for this use case. It only serves to hide the true dependencies of MyClass.

I'm not sure how to fix this, but it does seem like this class is doing too much, and has become a dumping ground for logic. If this is true, then MyClass could be on the fast track to becoming a God Object. I would start by trying to split this thing up into additional classes relevant to a use case.

6
  • 1
    Thank you. I edited the question to include the builder code (and make the MyClass constructor private, as it was supposed to be – I wrote the snippets on my smartphone). I don't believe it's a service locator Commented Sep 8 at 5:46
  • 1
    @SergeyZolotarev for it to stop being a service locator, you have to stop passing it as a constructor argument. Builder pattern is supposed to populate class members (usually via constructor, usually private), and can't be a member itself.
    – Basilevs
    Commented Sep 8 at 8:16
  • 1
    @SergeyZolotarev, for future reference, edits to your question that invalidate existing answers are discouraged. The edit certainly invalidates the paragraph about the service locator pattern, but the rest of my answer is unaffected, in my opinion. Commented Sep 8 at 17:22
  • @GregBurghardt but didn't Basilevs say it's still a service locator? Commented Sep 8 at 17:37
  • It depends on how it's used. Your original question was passing the "builder" as a constructor argument to MyClass, where the "builder" was merely returning dependencies. That is a service locator no matter what you call the class or variable. Commented Sep 8 at 17:56
1

Both builder pattern (unless it is a complex multistage one) and most DI containers are examples of late binding. They do their compliance checks in runtime. They deprive both service and clients of the most important benefit of a strong type system - design time checks.

IMO backward compatibility that occasionally fails in runtime is worse than no backward compatibility.

6
  • A correctly written builder will not create an object that fails compliance at runtime Commented Sep 8 at 15:42
  • @mattfreake it will fail to create the object. In runtime. See softwareengineering.stackexchange.com/a/454800/106122
    – Basilevs
    Commented Sep 8 at 19:06
  • It will gloriously statically enforce the rules of its mini language that you bake into the type system if you upgrade to a DSL and return differing types. But once you call build and validate it’s all happening at run time. Commented Sep 8 at 22:21
  • @candied_orange In the link the verbosity and inconvenience of static builder is demonstrated. It either inconvenient to use due to fixed order of fluent components, or very large in implementation (~ component count^2, supporting all operations on all stages).
    – Basilevs
    Commented Sep 9 at 7:41
  • Ugh, DSL implementation complexity is actually component count^3. Oops.
    – Basilevs
    Commented Sep 9 at 7:55
0

Suggestions

  1. Utilize versioning so you don't have to keep making these backwards compatible changes.
    // version 1.0
    public MyClass()
    // version 2.0
    public MyClass(String someArg, ServicA serviceA)
    
  2. Use a method other than the constructor to inject dependencies.
    // version 1.0
    public MyClass()
    
    // version 1.1
    public MyClass() {
         this(null);
    }
    public MyClass(String someArg)
    public SetServicA(ServicA serviceA)
    
  3. Maybe use stand-alone functions instead of a class.
  4. See if there is another programming language that handles this kind of problem better. With Go I don't run into this anymore.

Not the answer you're looking for? Browse other questions tagged or ask your own question.