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
  • 13
  • 19

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.

  • 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. –  Jul 11 '15 at 18:23
  • As a potential simplification, if the incompatibility is only an issue at the "final step" of the building, you could get away with making ImageBuilder.setSize() and setFilename() both return a FinalImageBuilder that has only a build() method on it. That would take very little boilerplate. – Ixrec Jul 11 '15 at 18:31
  • @Ixrec That is an option. For me, however, I enjoy the flexibility of being able to order the builder methods in any order. Typing in `setFilename("file.txt")` and then looking at the completion to be only `build()` would be surprising to me. –  Jul 11 '15 at 18:33
  • @Ixrec "the flexibility of being able to order the builder methods in any order"* is important to me too. – kdbanman Jul 11 '15 at 18:51
  • Do you think it's a problem that `SizeBuilder` and `FileBuilder` mirror each other's interface? That repetition sounds like an issue for maintenance. – kdbanman Jul 11 '15 at 18:51
  • @kdbanman it is the cost you pay to enforce this as part of the type system. They don't perfectly mirror each other - and that is the key part to how they work. For fun I tossed together a quick example (with an abstract class being constructed too) and placed it on GitHub: https://github.com/shagie/StatefulBuilder –  Jul 11 '15 at 19:06
  • To remove that final bit of overlap in the methods of `SizeBuilder` and `FileBuilder`, one can take the two methods `setRange` and `setStuff` into another parent interface, e.g. the `RangeAndStuffBuilder`, and then have `SizeBuilder` and `FileBuilder` inherit from it. (The meaning of `interface B extends interface A` is that any object *known to implement* the child interface B is *guaranteed to implement* the parent interface A also, which the compiler will help enforce.) – rwong Jul 11 '15 at 19:28
  • 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. –  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
  • @rwong That is indeed an option. There are a number of ways that such could be enforced (with interfaces). The simple ones might only need the approach described above. More complex ones could get quite interesting (thinking of those jUnit mock / verifier type libraries). In both of these situations though, the general Pattern is the same - enforcing state of the calling methods by using interfaces. –  Jul 11 '15 at 19:39
  • 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