This is a human interface question about combining the step builder pattern with the enhanced or wizard builder patterns into a creational DSL. It uses a fluent like interface, although it uses method chaining, not cascading. That is, the methods return differing types.
I’m confronting a monster class that has two constructors that take a mix of ints, Strings, and an array of Strings. Each constructor is 10 parameters long. It also has about 40 optional setters; a few of which conflict with each other if used together. Its construction code looks something like this:
Person person = Person("Homer","Jay", "Simpson","Homie", null, "black", "brown",
new Date(1), 3, "Homer Thompson", "Pie Man", "Max Power", "El Homo",
"Thad Supersperm", "Bald Mommy", "Rock Strongo", "Lance Uppercut", "Mr. Plow");
person.setClothing("Pants!!");
person.setFavoriteBeer("Duff");
person.setJobTitle("Safety Inspector");
This eventually fails because it turns out having set both favorite beer and job title is incompatible. Sigh.
Redesigning the monster class is not an option. It’s widely used. It works. I just don’t want watch it being constructed directly any more. I want to write something clean that will feed it. Something that will follow its rules without making developers memorize them.
Contrary to the wonderful builder patterns I’ve been studying this thing doesn’t come in flavors or categories. It demands some fields all the time and other fields when needed and some only depending on what has been set before. The constructors are not telescoping. They provide two alternate ways to get the class into the same state. They are long and ugly. What they want fed to them varies independently.
A fluent builder would definitely make the long constructors easier to look at. However, the massive number of optional setters clutters the required ones. And there is a requirement that a cascading fluent builder doesn’t satisfy: compile time enforcement.
Constructors force the developer to explicitly add required fields, even if nulling them. This is lost when using a cascading fluent builder. The same way it's lost with setters. I want a way to keep the developer from building until each required field has been added.
Unlike many builder patterns, what I’m after isn’t immutability. I’m leaving the class as I found it. I want to know the constructed object is in a good state just by looking at the code that builds it. Without having to refer to documentation. This means it needs to take the programmer thru conditionally required steps.
Person makeHomer(PersonBuilder personBuilder){ //Injection avoids hardcoding implementation
return personBuilder
// -- These have good default values, may be skipped, and don't conflict -- //
.doOptional()
.addClothing("Pants!!") //Could also call addTattoo() and 36 others
// -- All fields that always must be set. @NotNull might be handy. -- //
.doRequired() //Forced to call the following in order
.addFirstName("Homer")
.addMiddleName("Jay")
.addLastName("Simpson")
.addNickName("Homie")
.addMaidenName(null) //Forced to explicitly set null, a good thing
.addEyeColor("black")
.addHairColor("brown")
.addDateOfBirth(new Date(1))
.addAliases(
"Homer Thompson",
"Pie Man",
"Max Power",
"El Homo",
"Thad Supersperm",
"Bald Mommy",
"Rock Strongo",
"Lance Uppercut",
"Mr. Plow")
// -- Controls alternatives for setters and the choice of constructors -- //
.doAlternatives() //Either x or y. a, b, or c. etc.
.addBeersToday(3) //Now can't call addHowDrunk("Hammered");
.addFavoriteBeer("Duff")//Now can’t call addJobTitle("Safety Inspector");
.doBuild() //Not available until now
;
}
Person may be built after addBeersToday() since at that point all constructor info is known but is not returned until doBuild().
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, int beersToday,
String[] aliases);
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, String howDrunk,
String[] aliases);
These parameters set fields that must never be left with default values. beersToday and howDrunk set the same field different ways. favoriteBeer and jobTitle are different fields but cause conflicts with how the class is used so only one should be set. They are handled with setters not constructors.
The doBuild()
method returns a Person
object. It's the only one that does and Person
is the only type it will return. When it does Person
is fully initialized.
At each step of the interface the type returned is not always the same. Changing the type is how the developer is guided though the steps. It only offers valid methods. The doBuild()
method isn’t available until all needed steps have been completed.
The do/add prefixes are a kludge to make writing easier because the changing return type mismatches with the assignment and makes intelisense recommendations become alphabetical in eclipse. I've confirmed intellij doesn't have this problem. Thanks NimChimpsky.
This question is about the interface, so I'll accept answers that don't provide an implementation. But if you know of one, please share.
If you suggest an alternative pattern please show it's interface being used. Use all the inputs from the example.
If you suggest using the interface presented here, or some slight variation, please defend it from criticisms like this.
What I really want to know is if most people would prefer to use this interface to build or some other. This is human interface question. Does this violate PoLA? Don't worry about how hard it would be to implement.
However, if you are curious about implementations:
A failed attempt (didn't have enough states or understand valid vs not defaulted)
A step builder implementation (not flexible enough for multiple constructors or alternatives)
An enhanced builder (Still liner but has flexible states)
Wizard builder (Deals with forking but not remembering the path to pick a constructor)
Requirement:
- The monster (person) class is already closed to modification and extension; no touchy
Goals:
- Hide the long constructors since the monster class has 10 required parameters
- Determine which constructor to call based on alternatives used
- Disallow conflicting setters
- Enforce rules at compile time
Intent:
- Clearly signal when default values are not acceptable