2

I've looked at various definitions of the builder pattern and whilst there's varying definitions, they tend to be focused on the broad definition of incremental construction. However, it seems that most of the examples of builders I've ever seen essentially just construct "value" types with method chaining.

I've found, however, that it can occasionally be more useful to use a completely different sort of model in the builder to accomplish additional tasks. So, in addition to what people generally expect from a builder, this builder:

  1. Can hide details of the type being created
  2. Embedding complex build logic into the builder
  3. Using the same builder to create multiple similar types

I've put together a simple example of a builder variant that I created in Java that'll hopefully better demonstrate the ideas. (Note: 1 isn't really well demonstrated since it would take a lot more code to illustrate)

class FamiliesBuilder {
    final Map<String, List<String>> familyMap = new HashMap<>();
    String lastName;
    List<String> familyList;

    FamiliesBuilder newFamily(String lastName) {
        this.familyList = this.familyMap.getOrDefault(lastName, new LinkedList<>());
        this.familyMap.put(lastName, this.familyList);
        this.lastName = lastName;
        return this;
    }

    FamiliesBuilder addFamilyMember(String firstName) {
        this.familyList.add(firstName);
        return this;
    }

    Map<String, List<Person>> buildLastNameLookup() {
        final Map<String, List<Person>> res = new HashMap<>();
        for (Entry<String, List<String>> e : this.familyMap.entrySet()) {
            final String personLastName = e.getKey();
            final List<Person> people = new LinkedList<>();
            for (String firstName : e.getValue()) {
                people.add(new Person(firstName, personLastName));
            }

            res.put(e.getKey(), people);
        }

        return res;
    }

    List<Person> build() {
        List<Person> people = new LinkedList<>();
        for (Entry<String, List<String>> e : this.familyMap.entrySet()) {
            String personLastName = e.getKey();
            for (String personFirstName : e.getValue()) {
                people.add(new Person(personFirstName, personLastName));
            }
        }

        return people;
    }

    class Person {
        private String firstName;
        private String lastName;

        private Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }

    public static void main(String... args) {
        // Usage example
        List<Person> people = new FamiliesBuilder()
                .newFamily("Doe")
                .addFamilyMember("John")
                .addFamilyMember("Jane")
                .newFamily("Smith")
                .addFamilyMember("Tom")
                .build();
    }
}

I'm concerned that this variant is too far from the classic builder pattern that calling it a builder may be confusing and, if so, I'm wondering if there are any recognizable design patterns or techniques that accomplish a similar task.

  • Does it really have to conform to a well-known software pattern name for it to be useful to you? – Robert Harvey Jul 12 '18 at 21:00
  • 2
    @RobertHarvey: I agree that there is too much weight given to "named patterns". But I think the thrust of this specific question is whether an attempt to implement the pattern with the given characteristics is known to lead to trouble or not. There might be subtle ways in which this goes wrong that are not immediately apparent. – Eric Lippert Jul 12 '18 at 21:03
  • I don't need it be well known, I just don't want to call it something that it's not. And if it feels as though I'm square pegging a well understood problem into a builder when there's something more standard, I'd, of course, would like to know about it! – Kevin Raoofi Jul 12 '18 at 21:41
4

Short answer: I like it, and I make builders with the same characteristics. I don't think this is an anti-pattern at all.

Details:

Can hide details of the type being created

If the details are unimportant, confusing or error-prone for the user of the type, then absolutely hide them in the builder. The builder is the expert so that the consumer of the type doesn't have to be.

Embedding complex build logic into the builder

Is the complexity fundamental to the type, because the "business domain" of the code is itself complex? Then absolutely, encapsulate the complexity somewhere. Whether it is in the builder or the type being built, doesn't really matter to me but it should go somewhere.

Using the same builder to create multiple similar types

This is your weakest point. Code re-use is overrated and generality has many costs. Often it is cheaper and easier to write two specific solutions than to write one general solution and specialize it two ways.

  • I was about to argue with point 3 by talking about how the two types are extremely similar and have a 1 to 1 relationship. However, upon further thought, I think you're right. And if they're so similar, it would probably be better design to provide a utility to convert one to the other, anyway. – Kevin Raoofi Jul 12 '18 at 21:46
1

I don't think the example you provided is the Builder pattern at all.

The Builder pattern is a way of creating an immutable object that has many optional parameters. The main reason for it is so that you don't need to have a ridiculous number of constructors (or factory methods) due to all the possible combinations. (It also provides the benefit of method chaining which can make the code easier to read).

If the objects are not immutable the pattern serves no real purpose as the object can be created and have the desired parameter set later, either through direct access to the field or through setter methods.

In the example you gave you are really just wrapping around a List<Person> object and providing functionality to add to it as well as providing a way to get a HashMap>.

In this instance a better way of doing this would be:

public class Family {
    private String lastName;
    private String[] firstNames;

    //alternatively there can be a List<Person> field instead of lastName and firstNames

    public Family(String lastName, String firstNames){
        //code omitted...        
    }

    public static HashMap<String, List<Person>> getLastNameLookup<Collection<Family>> families) {
        //code omitted...
    }
}

The code could then be used like this:

List<Family> families = new ArrayList<>();
families.add(new Family("Doe", new String[] { "John", "Jane" });
families.add(new Family("Smith", new String[] { "John" });

HashMap<String, List<Person>> lastNameLookup = Family.getLastNameLookup(families);

In summary, I do think it is an anti-pattern because the added complexity doesn't really provide any value. There is no information being hidden here that couldn't be hidden just as well in the class itself. I think the Builder pattern should only be used to create immutable objects that can have many optional values as a way to prevent needing to introduce a very large number of constructors or factory methods.

  • Sorry, my example definitely does fail to reduce complexity. However, in my real world variant, there is lots of complexity because the type being built requires multiple other types that are also difficult to build. The main alternative, I suppose, is to provide easier means to build the other types, but I found little reason to do so. I do understand and appreciate your view that builders should not be complex and to defer work to utility methods, instead. – Kevin Raoofi Jul 12 '18 at 21:38

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.