Is there something like a “build-once” builder?

our framework provides some kind of “log” objects; which actually represent legacy structures in our legacy C/C++ code. Today I started writing a helper class that uses the builder pattern to allow a more modern-age approach for creating such log objects.

But there is one question for which I am not sure what the correct answer is:

basically these log objects have some attributes that are “required”; so following the builder pattern, you have to specify these arguments as arguments for ctor of my new builder class. Then the builder has a some methods to configure “optional attributes”; and of course a method “build()” that will emit a log object with the summary of the collected values.

But now I am wondering: should I care if people would be re-using the same builder object? So, should I have a “reset()” method to clear all optional arguments; or prevent that build() is called more than once? Or better not worry not all?

Answer

should I care if people would be re-using the same builder object?

Well you want to watch for side effects, but I think that’s why you are asking, because you’re aware that people/code will leave junk in the builder.

So, should I have a “reset()” method to clear all optional arguments;

I think that if you pass a builder around, the responsibility to clean up should be on the consumer (the function or class that takes the builder as input). So that we are not causing side effects. Example:

public class SomeClass {
    public SomeClass(Log.Builder builder){
       builder.tag("SomeClass"); //bad, we caused a side effect
                                 //someone class forgets to set the tag
                                 //later, then they get this tag
       log = builder.build();
    }
}

One solution would be to provide a copy constructor:

public class SomeClass {
    public SomeClass(Log.Builder builder){
       Log.Builder localBuilder = new Log.Builder(builder); //copy constructor being used.
       localBuilder.tag("SomeClass"); //only applies to the local builder
       log = localBuilder.build();
    } //builder is as it was before the call, no side effect
}

You can even call SomeClass with a copy, so that you don’t need to care if it was implemented to be side effect free or not:

Log.Builder defaults = new Log.Builder(...whatever...).tag("notag");
new SomeClass(new Log.Builder(defaults));
new SomeClass2(new Log.Builder(defaults));

To lock this down further, you could have an interface Log.ReadOnlyBuilder which doesn’t have setters and is really only good for building or creating a copy:

public class Log {
    public interface ReadOnlyBuilder {
        Builder copy();
        Log build();
    }

    public static class Builder implements ReadOnlyBuilder {
        Builder tag(String tag){
          ...
        }
    }
}

public class SomeClass {
    public SomeClass(Log.ReadOnlyBuilder builder){
       log = builder.copy().tag("SomeClass").build();
    } //builder is as it was before the call, no side effect
}

I like this, because the responsibility is back on the consumer to not cause side effect yet it is enforced though types and interfaces. Plus if they do not need to set any fields, they can just use the readonly builder, we haven’t had to make a copy just in case.

P.S. I don’t think optional and required is the division point around which you want to decide what to reset and what to set. I think the copy constructor should just copy every field in the builder to the new builder. But you are free to develop copy method/constructor however you like.

Leave a Reply

Your email address will not be published. Required fields are marked *