The Chained Creator Pattern

Sometimes a little extra care around a certain part of the code can really pay off. In one case, you want to achieve higher quality by reducing the number of errors and make the code easier to work with. In another case, the code is used in many places so it will pay off if you can simplify the API. If you build a framework that is used by a lot of people, then a small improvement makes a huge difference.

The purpose of this pattern is to simplify a very common occurrence, the creation of class instances, by making the code cleaner and more readable. It can be used to create ordinary class instances and also replace long parameter lists (if you can't avoid them) with a "parameter class".

Most of the deficiencies this pattern addresses can also be resolved by named parameters and default arguments, supported by some languages. However, you can still benefit by using this pattern even if your language has support for these two constructions.

The only real downside of the pattern is that you need to add some boilerplate code in the Creator.

The upsides are more numerous:
  • Lets the code communicate What instead of How, for example Car.create().defaults() instead of new Car().
  • View the name of the arguments, which makes the code more readable.
  • Support for default values anywhere in the parameter list, not just at the end.
  • Make use of enumerated parameters more readable with less boiler-plate code.
  • Change the order of parameters with same type without the risk of introducing errors.
  • One constructor with support for different paths, no need for a set of constructors.
  • Guarantees that all attributes in the object are set.
Let’s look at some examples (the source is hosted at GitHub):
package nu.tengstrand.chainedcreator;

import nu.tengstrand.chainedcreator.book.*;
import nu.tengstrand.chainedcreator.car.Car;

public class Main {
    /**
     * This is an example of the pattern Chained Creator.
     *
     * Author: Joakim Tengstrand
     */
    public static void main(String[] args) {
        bookExampleUsingPrimitiveTypesNotUsingChainedCreator();
        bookExampleUsingValueObjectsNotUsingChainedCreator();

        bookExampleNoDefauls();
        bookExampleWithAllDefaults();
        bookExampleWithSomeDefaults();

        bookExampleUsingAMixOfPrimitivesAndValueObjects();
        carExampleSendingInPrimitiveTypesInClassWithPrimitiveAttributes();
    }

    // Example 1 - regular constructor
    private static void bookExampleUsingPrimitiveTypesNotUsingChainedCreator() {
        Book book = new Book("Clean Code", BookBinding.PAPERBACK, "Robert C Martin", 
                431, 660);
        printExample(1, book);
    }

    // Example 2 - regular constructor
    private static void bookExampleUsingValueObjectsNotUsingChainedCreator() {
        Book book = new Book(new BookTitle("The Pragmatic Programmer"),
                BookBinding.PAPERBACK,
                new BookAuthor("Andrew Hunt, David Thomas"),
                new BookNumberOfPages(352),
                new BookWeightInGrams(540));
        printExample(2, book);
    }

    // Example 3
    private static void bookExampleNoDefauls() {
        Book book = Book.create().title("Test Driven").bindingPaperback()
                .author("Lasse Koskela").numberOfPages(544).weighInGrams(1180);
        printExample(3, book);
    }

    // Example 4
    private static void bookExampleWithAllDefaults() {
        Book book = Book.create().defaults();
        printExample(4, book);
    }

    // Example 5
    private static void bookExampleWithSomeDefaults() {
        // Using default values for 'binding', 'author' and 'weightInGrams'.
        Book book = Book.create().title("Thin book").defaultBinding()
                .unknownAuthor().numberOfPages(125).defaultWeightInGrams();
        printExample(5, book);
    }

    // Example 6
    private static void bookExampleUsingAMixOfPrimitivesAndValueObjects() {
        Book book = Book.create()
                .title(new BookTitle("Programming in Scala, 2nd Edition"))
                .bindingPaperback()
                .author("Martin Odersky, Lex Spoon, Bill Venners")
                .numberOfPages(883)
                .weighInGrams(1134);
        printExample(6, book);
    }

    // Example 7
    private static void carExampleSendingInPrimitiveTypesInClassWithPrimitiveAttributes() {
        Car car = Car.create().name("Lamborghini").color("Red").length(458);
        printExample(7, car);
    }

    private static void printExample(int example, Object instance) {
        System.out.println(example + ". " + instance);
    }
}

Output:
1. Book{title='Clean Code', author='Robert C Martin', numberOfPages=431, weighInGrams=660}
2. Book{title='The Pragmatic Programmer', author='Andrew Hunt, David Thomas', numberOfPages=352, weighInGrams=540}
3. Book{title='Test Driven', author='Lasse Koskela', numberOfPages=544, weighInGrams=1180}
4. Book{title='UNKNOWN TITLE', author='UNKNOWN AUTHOR', numberOfPages=2, weighInGrams=200}
5. Book{title='Thin book', author='UNKNOWN AUTHOR', numberOfPages=125, weighInGrams=200}
6. Book{title='Programming in Scala, 2nd Edition', author='Martin Odersky, Lex Spoon, Bill Venners', numberOfPages=883, weighInGrams=1134}
7. Car{name='Lamborghini', color='Red', length=458}

Example 1 and 2 shows regular constructors. Example 3 to 7 shows usage of this pattern, Chained Creator.

Example 1

The problem here is that it's not clear what the arguments 431, 660 (line 27) is representing.

Example 2

There is no doubt what the different parameters represents, but the argument list is not very clean.

Example 3

The syntax is very readable and clean, using the pattern Chained Creator.

Example 4

Example of how to set all the class' attributes to default values.

Example 5

Example of how to set some of the arguments to default values.

Example 6

In this example we use bindingPaperback() instead of binding(BookBinding.PAPERBACK) to improve the user experience of the API.

Example 7

A chained creator with only primitive data types used by the class Car consisting solely of primitives. Here we take advantage of Java's ability to reference the enclosing class (if implemented in a languages without support for this, you would need to provide a reference of Car to your parameter classes):

package nu.tengstrand.chainedcreator.car;

public class Car {
    private String name;
    private String color;
    private int length;

    // Package-private access level
    Car() {
    }

    // Creates an instance of a Car
    public static Name create() {
        return new Car().new Name();
    }

    // Example of not using a separate creator class, and take advantage of non-static
    // inner classes in Java who can can access attributes of the enclosing class.
    //
    // Parameter chain. If you have a lot of attributes, this is an example of a more compact format.
    public class Name { public Color name(String name) { Car.this.name = name; return new Color(); }}
    public class Color { public Length color(String color) { Car.this.color = color; return new Length(); }}
    public class Length { public Car length(int length) { Car.this.length = length; return Car.this; }}

    @Override
    public String toString() {
        return "Car{" + "name='" + name + "', color='" + color + "', length=" + length + '}';
    }
}

The trick here is to set the constructor to package-private (line 9) and add the public static method create(), at line 13. If your instinctive reaction is to not allow Car to create instances of itself, keep in mind that the class already has this responsibility by providing the ability to write new Car(). With that said, we don't want to introduce a CarFactory which would also make the code less readable.

In example 3 to 6 we are introducing the class Book:
package nu.tengstrand.chainedcreator.book;

/**
 * This is an example how you can use the pattern Chained Creator
 * with a mix of value objects and primitive data types.
 */
public class Book {
    BookTitle title;
    BookBinding binding;
    BookAuthor author;
    BookNumberOfPages numberOfPages;
    BookWeightInGrams grams;

    BookCreator creator = new BookCreator(this);

    // Package-private access level
    Book() {
    }

    // Example of a regular constructor, used in Example 1. 
    public Book(String title, BookBinding binding, String author, int numberOfPages, int grams) {
        this(new BookTitle(title), binding, new BookAuthor(author), new BookNumberOfPages(numberOfPages), new BookWeightInGrams(grams));
    }
    
    // Example of a regular constructor, used in Example 2. 
    public Book(BookTitle title, BookBinding binding, BookAuthor author, BookNumberOfPages numberOfPages, BookWeightInGrams grams) {
        this.title = title;
        this.binding = binding;
        this.author = author;
        this.numberOfPages = numberOfPages;
        this.grams = grams;
    }

    // Create a book by using the pattern Chained Creator.
    public static BookCreator.Title create() {
        return new Book().creator.create();
    }

    @Override
    public String toString() {
        return "Book{title='" + title + "', author='" + author + "', numberOfPages=" + numberOfPages + ", weighInGrams=" + grams + "}";
    }
}

To use the pattern Chained Constructor you should not implement the constructors at line 20 to 32, they are added to have something to compare with (used in example 1 and 2). In this example, the class Book contains only value object (line 8 to 12), but you are free to choose how your class is represented internally.

When you have added your class variables you also need to add the BookCreator instance variable as package-private (line 14) and a public static create() method (line 35).

The next thing to do is to implement the class BookCreator:
package nu.tengstrand.chainedcreator.book;

public class BookCreator {
    private Book book;

    public BookCreator(Book book) {
        this.book = book;
    }

    /**
     * Returns the first argument in the chain.
     */
    public Title create() {
        return new Title();
    }

    public class Title {
        public Binding title(String title) {
            return title(new BookTitle(title));
        }
        public Binding title(BookTitle title) {
            book.title = title;
            return new Binding();
        }
        public Book defaults() {
            title("UNKNOWN TITLE");
            new Binding().defaultBinding();
            new Author().unknownAuthor();
            book.numberOfPages = new BookNumberOfPages(2);
            new WeightInGrams().defaultWeightInGrams();
            return book;
        }
    }

    public class Binding {
        public Author binding(BookBinding binding) {
            book.binding = binding;
            return new Author();
        }
        public Author defaultBinding() {
            return bindingPaperback();
        }
        // An example of how to make the constructor chain more readable,
        // bindingPaperback() instead of binding(BookBinding.PAPERBACK).
        public Author bindingPaperback() {
            return binding(BookBinding.PAPERBACK);
        }
        public Author bindingHardback() {
            return binding(BookBinding.HARDBACK);
        }
    }

    public class Author {
        public NumberOfPages author(String author) {
            book.author = new BookAuthor(author);
            return new NumberOfPages();
        }

        public NumberOfPages unknownAuthor() {
            return author("UNKNOWN AUTHOR");
        }
    }

    public class NumberOfPages {
        public WeightInGrams numberOfPages(int numberOfPages) {
            book.numberOfPages = new BookNumberOfPages(numberOfPages);
            return new WeightInGrams();
        }
    }

    public class WeightInGrams {
        public Book weighInGrams(int grams) {
            book.grams = new BookWeightInGrams(grams);
            return book;
        }

        public Book defaultWeightInGrams() {
            return weighInGrams(BookWeightInGrams.DEFAULT_WEIGHT);
        }
    }
}

Start with adding the instance variable to the class Book (line 4) and the constructor (line 6). My experience is that it is easiest to start with the last argument in the list, so let's add the inner class WeightInGrams (line 71). We want this parameter to support sending in weight in grams as an int and we also want it to handle setting a default value. We therefore add the two methods at line 72 and 77 and let them return a Book.

Continue with the second last parameter, by adding the class NumberOfPages (line 64). We decide not to handle default values for this parameter. Add the method numberOfPages(int) and let it return an instance to the next class in the constructor chain WeightInGrams.

Continue with all other parameters, Author, Binding and Title. Finish with creating the create() method (line 13). Now we are done!

With this pattern you need to do some extra work with the code that handles the creation of a class, but in return you get more control of every parameter in the parameter list.

Best regards,
Joakim Tengstrand