9

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?

kdbanman
  • 1,447

1 Answers1

12

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.