8

This is motivated by this answer to a separate question.

The builder pattern is used to simplify complex initialization, especially with optional initialization parameters). But I don't know how to properly manage mutually exclusive configurations.

Here's an Image class. Image can be initialized from a file or from a size, but not both. Using constructors to enforce this mutual exclusion is obvious when the class is simple enough:

public class Image
{
    public Image(Size size, Thing stuff, int range)
    {
    // ... initialize empty with size
    }

    public Image(string filename, Thing stuff, int range)
    {
        // ... initialize from file
    }
}

Now assume Image is actually configurable enough for the builder pattern to be useful, suddenly this might be possible:

Image image = new ImageBuilder()
                  .setStuff(stuff)
                  .setRange(range)
                  .setSize(size)           // <----------  NOT
                  .setFilename(filename)   // <----------  COMPATIBLE
                  .build();

These problems must be caught at run time rather than at compile time, which isn't the worst thing. The problem is that consistently and comprehensively detecting these problems within the ImageBuilder class could get complex, especially in terms of maintenance.

How should I deal with incompatible configurations in the builder pattern?

11

You've got your Builder. However, at this point you need some interfaces.

There is a FileBuilder interface that defines one subset of methods (not setSize) and a SizeBuilder interface that defines another subset of methods (not setFilename). You may wish to have a GenericBuilder interface extend the FileBuilder and SizeBuilder - it is not necessary though some people may prefer that approach.

The method setSize() returns a SizeBuilder. The method setFilename() returns a FileBuilder.

The ImageBuilder has all the logic for both setSize() and setFileName(). However, the return type for these would specifiy the appropriate subset interface.

class ImageBulder implements FileBuilder, SizeBuilder {
    ImageBuilder() {
        doInitThings;
    }

    ImageBuilder setStuff(Thing) {
        doStuff;
        return this;
    }

    ImageBuilder setRange(int range) {
        rangeStuff;
        return this;
    }

    SizeBuilder setSize(Size size) {
        stuff;
        return this;
    }

    FileBuilder setFilename(String filename) {
        otherStuff;
        return this;
    }

    Image build() {
        return new Image(...);
    }
}

One special bit here is that once you have a SizeBuilder, all of the returns need to be SizeBuilders. The interface for it looks like:

interface SizeBuilder {
    SizeBuilder setRange(int range);
    SizeBuilder setSize(Size size);
    SizeBuilder setStuff(Thing stuff);
    Image build();
}

interface FileBuilder {
    FileBuilder setRange(int range);
    FileBuilder setFilename(String filename);
    FileBuilder setStuff(Thing stuff);
    Image build();
}

Thus, once you call one of those methods, you are now unable to call the other and create an object with an invalid state.

  • Really interesting, thank you. I'm a bit confused on how these would be used though. Specifically, I can't figure out what the declaration and initialization types would be. I'm probably just imagining things much more complicated than necessary. Could you include a usage example? – kdbanman Jul 11 '15 at 18:17
  • The image builder returns the interface corresponding to the state change which that method calls. However, once you get back a specific interface from ImageBuilder, future calls against that object are done on that interface which restricts the ability to call incompatible methods. – user40980 Jul 11 '15 at 18:23
  • 1
    @rwong while I'll admit to not looking into it too deeply, the problem that I thought I had with such an approach was that the 'state' of the builder could get reset. One would need to make sure that once setSize() was called, all further builder invocations were on SizeBuilder. If the type of setRange() was not the SizeBuilder or something that extends/implements that one could get around to calling setFilename on it again. You also have the situation (not described here) where instead of size you have int width and int height such that both need to be called. – user40980 Jul 11 '15 at 19:33
  • 1
    @MichaelT Given the intricate issues of circumvention, I suspect that enforcing a strict order of parameter initialization (resulting in a prefix tree of parameter items) might be a good thing when using the builder pattern. As a result, common parameter items such as Range and Stuff must be initialized at first, not at arbitrary times. – rwong Jul 11 '15 at 19:36
  • 1
    @MichaelT: at that point, LSP comes into play. You can be sure the methods of the apparent type (RangeAndStuffBuilder) can be called on actual type. Further restrictions can be implemented by returning more basal types for some methods (though this will cause an exponential increase in types), effectively removing operations. As long as method results don't go back down the hierarchy, you won't get type errors. The setHeight/setWidth scenario could be implemented with a sibling hierarchy that doesn't have a build method. – outis Jul 11 '15 at 22:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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