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.