Builder (Joshua Bloch-style) for concrete implemen

2019-07-02 00:13发布

问题:

Let's say I have an abstract class (BaseThing). It has one required parameter ("base required") and one optional parameter ("base optional"). I have a concrete class that extends it (Thing). It also has one required parameter ("required") and one optional parameter ("optional"). So something like:

public abstract class BaseThing {
    public static final String DEFAULT_BASE_OPTIONAL = "Default Base Optional";

    private final String baseRequired;
    private String baseOptional = DEFAULT_BASE_OPTIONAL;

    protected BaseThing(final String theBaseRequired) {
        this.baseRequired = theBaseRequired;
    }

    final void setBaseOptional(final String newVal) {
        this.baseOptional = newVal;
    }

    public final void selfDescribe() {
        System.out.println("Base Required: " + baseRequired);
        System.out.println("Base Optional: " + baseOptional);

        selfDescribeHook();
    }

    protected abstract void selfDescribeHook();
}

and:

public final class Thing extends BaseThing {
    public static final String DEFAULT_OPTIONAL = "Default Optional";

private final String required;
    private String optional = DEFAULT_OPTIONAL;

    Thing(final String theRequired, final String theBaseRequired) {
        super(theBaseRequired);
        required = theRequired;
    }

    @Override
    protected void selfDescribeHook() {
        System.out.println("Required: " + required);
        System.out.println("Optional: " + optional);
    }

    void setOptional(final String newVal) {
        optional = newVal;
    }
}

I want to have a Joshua Bloch-style builder for Thing objects. More generally, though, I want to make it easy for concrete implementations of BaseThing to have builders, so what I really want (I think) is a BaseThing builder that can easily be used to make a ThingBuilder, or an OtherThingBuilder, or a SuperThingBuilder.

Is there a better way than the following that I've come up with (or are there problems with what I've come up with)?

public abstract class BaseThingBuilder<T extends BaseThing> {
    private String baseOptional = BaseThing.DEFAULT_BASE_OPTIONAL;

    public BaseThingBuilder<T> setBaseOptional(final String value) {
        baseOptional = value;
        return this;
    }

    public T build() {
        T t = buildHook();
        t.setBaseOptional(baseOptional);

        return t;
    }

    protected abstract T buildHook();
}

and:

public final class ThingBuilder extends BaseThingBuilder<Thing> {
    private final String baseRequired;
    private final String required;
    private String optional = Thing.DEFAULT_OPTIONAL;

    public ThingBuilder(final String theRequired,
            final String theBaseRequired) {
        required = theRequired;
        baseRequired = theBaseRequired;
    }

    public ThingBuilder setOptional(final String value) {
        optional = value;
        return this;
    }

    protected Thing buildHook() {
        Thing thing = new Thing(required, baseRequired);
        thing.setOptional(optional);

        return thing;
    }
}

Which can be used to build Thing objects in a manner similarly to the following:

        BaseThingBuilder<Thing> builder = 
                new ThingBuilder("Required!", "Base Required!")
                    .setOptional("Optional!")
                    .setBaseOptional("Base Optional!");
        Thing thing = builder.build();
        thing.selfDescribe();

Which outputs:

Base Required: Base Required!
Base Optional: Base Optional!
Required: Required!
Optional: Optional!

One issue that I know about, but that I don't consider particularly important (though if it can be improved it would be nice to do so) is that you have to set all non-base options before you set any base option: Doing otherwise would result in a syntax error, as setBaseOptional() returns a BaseThingBuilder rather than a ThingBuilder.

Thanks in advance.

回答1:

I don't think it's a good idea to think of builders that way. A hierarchy of builders usually leads to headaches and fragile code.

Cutting down the amount of code that needs to be written in the concrete builders and reusing logic from the base builder is closely tied to the domain. It's not easy to develop a general solution. But, let's try to go through an example anyway:

public interface Builder<T> {
  T build();
}

public class Person {
  private final String name;

  //the proper way to use a builder is to pass an instance of one to
  //the class that is created using it...
  Person(PersonBuilder builder) {
    this.name = builder.name;
  }

  public String getName(){ return name; }

  public static class PersonBuilder implements Builder<Person> {
    private String name;
    public PersonBuilder name(String name){ this.name = name; return this; }

    public Person build() {
      if(name == null) {
        throw new IllegalArgumentException("Name must be specified");
      }
      return new Person(this);
    }
  }
}

Groovy, baby! Now what? Maybe you want to add a class to represent a student. What do you do? Do you extend Person? Sure, that's valid. How about taking a more "strange" route and attempting aggregation? Yep, you can do that too... Your choice would have an affect on how you will end up implementing builders. Let's say you stick to the traditional path and extend Person (you should already starting asking yourself, does it make sense for Person to be a concrete class? If I make it abstract, do I really need a builder? If the class is abstract should the builder be abstract?):

public class Student extends Person {
  private final long id;

  Student(StudentBulder builder) {
    super(builder);
    this.id = builder.id;
  }

  public long getId(){ return id; }

  //no need for generics, this will work:
  public static class StudentBuilder extends PersonBuilder {
    private long id;
    public StudentBuilder id(long id){ this.id = id; return this; }

    public Student build() {
      if(id <= 0) {
        throw new IllegalArgumentException("ID must be specified");
      }
      return new Student(this);
    }
  }
}

Ok, this looks exactly like what you wanted! So, you try it:

Person p = new PersonBuilder().name("John Doe").build();
Student s = new StudentBuilder().name("Jane Doe").id(165).build();

Looks great! Except, it doesn't compile... There's an error at line 2 and it states The method id(int) is undefined for the type Person.PersonBuilder. The problem is that PersonBuilder#name returns a builder of type PersonBuilder, which isn't what you want. In StudentBuilder you actually want the return type of name to be StudentBuilder. Now, you think ahead and realize that if anything extends StudentBuilder you'd want it to return something else entirely... Is that doable? Yes, with generics. However, it's ugly as hell and introduces quite a bit of complexity. Therefore, I refuse to post the code that illustrates it, for the fear that someone will see this thread and actually use it in their software.

You might think rearranging method calls will work (calling id before calling name): new StudentBuilder().id(165).name("Jane Doe").build(), but it won't. At least not without an explicit cast to Student: (Student)new StudentBuilder().id(165).name("Jane Doe").build() since, in this case, PersonBuilder#build is being called which has a return type of Person... This is simply unacceptable! Even if it worked without an explicit cast, it should make you wince to know that a builder's methods must be called in a certain order. Because if you don't, something won't work...

There are many more problems that would arise if you continue trying to get it to work. And even if you did get it to work, I don't think it would be easily comprehensible and certainly not elegant. Of course, feel free to prove me wrong and post your solution here.

By the way, you should also ask yourself what is an abstract builder? Because, it sounds like an oxymoron.

In the end, I believe that the scope of this question is too great. The answer is domain-specific and hard to come up with in the absence of your requirements. Just remember, the general guideline for builders is to have them be as simple as possible.

Also, take a look at a related question.



回答2:

As far as I can tell if you remove the generics then

BaseThingBuilder<Thing> builder = 
            new ThingBuilder("Required!", "Base Required!")

changes to

BaseThingBuilder builder = 
            new ThingBuilder("Required!", "Base Required!")

The rest of it all remains same, including the restriction that subclass has to be initialized first. So I really don't think this warrants use of generics. Maybe I am missing something.

I seem to remember something like this from Bjarne Stroustrup, long back...