Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set a value at most once with the builder pattern

Is there a standard practice in Java, while using the builder pattern, to ensure that a member variable is set at most once. I need to make sure that the setter is called 0 or 1 times but never more. I would like to throw a RuntimeException of some type but am worried about synchronization issues as well as best-practices in this area.

like image 578
bigfy Avatar asked Aug 26 '14 03:08

bigfy


2 Answers

There's nothing wrong with raising an exception if a user calls a method in an illegal way like you describe, but it's not terribly elegant. The idea behind the builder pattern is to let users write fluent, readable object definitions, and compile-time safety is a big part of that. If users can't be confident the builder will succeed even if it compiles, you're introducing additional complexity users now need to understand and account for.

There are a couple of ways to accomplish what you're describing, lets explore them:

  1. Just let users do what they want

    One nice thing about builders is they can let you construct multiple different objects from the same builder:

    List<Person> jonesFamily = new ArrayList<>();
    Person.Builder builder = new Person.Builder().setLastName("Jones");
    
    for(String firstName : jonesFamilyFirstNames) {
      family.add(builder.setFirstName(firstName).build());
    }
    

    I assume you have a good reason for forbidding this sort of behavior, but I'd be remiss if I didn't call out this useful trick. Maybe you don't need to restrict this in the first place.

  2. Raise an Exception

    You suggest raising an exception, and that will certainly work. Like I said, I don't think it's the most elegant solution, but here's one implementation (using Guava's Preconditions, for extra readability):

    public class Builder {
      private Object optionalObj = null;
      // ...
    
      public Builder setObject(Object setOnce) {
        checkState(optionalObj == null, "Don't call setObject() more than once");
        optionalObj = setOnce;
      }
      // ...
    }
    

    This raises an IllegalStateException, so you can just call throw new IllegalStateException() if you aren't using Guava (you should be... :) ). Assuming you're not passing builder objects around between threads, you should have no synchronization issues. If you are, you should put some further thought into why you need the same builder in different threads - that's almost surely an anti-pattern.

  3. Don't provide the method at all

    This is the cleanest, clearest way to prevent the user from calling a method you don't want them to - don't provide it in the first place. Instead, override either the builder's constructor or build() method so they can optionally pass the value at that time, but at no other time. This way you clearly guarantee the value can be set at most once per object constructed.

    public class Builder {
      // ...
    
      public Obj build() { ... }
      public Obj build(Object onceOnly) { ... }
    }
    
  4. Use different types to expose certain methods

    I haven't actually done this, and it might be more confusing than it's worth (in particular, you'll likely need to use a self-bounding generic for the methods in Builder), but it came to mind while I was writing and could be very explicit for certain use cases. Have your restricted method in a subclass of the builder, and that method returns the parent type, therefore preventing the caller from re-calling the method. An example might help, if that didn't make sense:

    public class Builder {
      // contains regular builder methods
    }
    
    public class UnsetBuilder extends Builder {
      public Builder setValue(Object obj) { ... }
    }
    
    // the builder constructor actually returns an UnsetBuilder
    public static UnsetBuilder builder() { ... }
    

    Then we can call something like:

    builder().setValue("A").build();
    

    But we'd get a compile-time error if we tried to call:

    builder().setValue("A").setValue("B").build();
    

    because setValue() returns Builder, which lacks a setValue() method, therefore preventing the second case. This would be tricky to get exactly right (what if the user casts the Builder back to a UnsetBuilder?) but with some effort would do what you're looking for.

like image 122
dimo414 Avatar answered Sep 23 '22 05:09

dimo414


Object objectToSet;

boolean isObjectSet = false;

void setObject(Object object) throws RuntimeException {
    if(!isObjectSet) {
        objectToSet=object;
        isObjectSet=true;
    } else {
       throw new RuntimeException();
    }

}

This is the standard practice.

like image 21
BAR Avatar answered Sep 25 '22 05:09

BAR