21

It is well know that if your class have a constructor with many parameters, say more than 4, then it is most probably a code smell. You need to reconsider if the class satisfies SRP.

But what if we build and object that depends on 10 or more parameters, and eventually end of with setting all those parameters through Builder pattern? Imagine, you build a Person type object with its personal info, work info, friends info, interests info, education info and so on. This is already good, but you somehow set that same more than 4 parameters, right? Why this two cases are not considered to be the same?

25

The Builder Pattern does not solve the “problem” of many arguments. But why are many arguments problematic?

  • They indicate your class might be doing too much. However, there are many types that legitimately contain many members that cannot be sensibly grouped.
  • Testing and understanding a function with many inputs gets exponentially more complicated – literally!
  • When the language does not offer named parameters, a function call is not self-documenting. Reading a function call with many arguments is quite difficult because you have no idea what the 7th parameter is supposed to do. You wouldn't even notice if the 5th and 6th argument were swapped accidentally, especially if you're in a dynamically typed language or everything happens to be a string, or when the last parameter is true for some reason.

Faking named parameters

The Builder Pattern addresses only one of these problems, namely the maintainability concerns of function calls with many arguments. So a function call like

MyClass o = new MyClass(a, b, c, d, e, f, g);

might become

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

∗ The Builder pattern was originally intended as a representation-agnostic approach to assemble composite objects, which is a far greater aspiration than just named arguments for parameters. In particular, the builder pattern does not require a fluent interface.

This offers a bit of extra safety since it will blow up if you invoke a builder method that doesn't exist, but it otherwise does not bring you anything that a comment in the constructor call wouldn't have. Also, manually creating a builder requires code, and more code can always contain more bugs.

In languages where it is easy to define a new value type, I've found that it's way better to use microtyping/tiny types to simulate named arguments. It is named so because the types are really small, but you end up typing a lot more ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Obviously, the type names A, B, C, … should be self-documenting names that illustrate the meaning of the the parameter, often the same name as you'd give the parameter variable. Compared with the builder-for-named-arguments idiom, the required implementation is a lot simpler, and thus less likely to contain bugs. For example (with Java-ish syntax):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

The compiler helps you guarantee that all arguments were provided; with a Builder you'd have to manually check for missing arguments, or encode a state machine into the host language type system – both would likely contain bugs.

There is another common approach to simulate named arguments: a single abstract parameter object that uses an inline class syntax to initialize all fields. In Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

However, it is possible to forget fields, and this is a quite language-specific solution (I've seen uses in JavaScript, C#, and C).

Fortunately, the constructor can still validate all arguments, which is not the case when your objects are created in a partially-constructed state, and require the user to provide further arguments via setters or an init() method – those require the least coding effort, but make it more difficult to write correct programs.

So while there are many approaches to address the “many unnamed parameters make code difficult to maintain problem”, other problems remain.

Approaching the root problem

For example the testability problem. When I write unit tests, I need the ability to inject test data, and to provide test implementations to mock out dependencies and operations that have external side effects. I can't do that when you instantiate any classes within your constructor. Unless the responsibility of your class is the creation of other objects, it shouldn't instantiate any non-trivial classes. This goes hand in hand with the single responsibility problem. The more focussed the responsibility of a class, the easier it is to test (and often easier to use).

The easiest and often best approach is for the constructor to take fully-constructed dependencies as parameter, though this shoves the responsibility of managing dependencies to the caller – not ideal either, unless the dependencies are independent entities in your domain model.

Sometimes (abstract) factories or full dependency injection frameworks are used instead, though these might be overkill in the majority of use cases. In particular, these only reduce the number of arguments if many of these arguments are quasi-global objects or configuration values that don't change between object instantiation. E.g. if parameters a and d were global-ish, we'd get

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Depending on the application, this might be a game-changer where the factory methods end up having nearly no arguments because all can be provided by the dependency manager, or it might be a large amount of code that complicates instantiation for no apparent benefit. Such factories are way more useful for mapping interfaces to concrete types than they are for managing parameters. However, this approach tries to addresses the root problem of too many parameters rather than just hiding it with a pretty fluent interface.

| improve this answer | |
  • I would really argue against self-documenting part. If you have a decent IDE + decent comments, most of the time the constructor and parameter definition is one keystroke away. – JavierIEH Jun 1 '17 at 0:56
  • In Android Studio 3.0, the parameter name in the constructor is shown adjacent to the value being passed in a constructor call. e.g: new A(operand1: 34, operand2: 56); operand1 and operand2 are the paramter names in the constructor. They are shown by the IDE to make the code more readable. So, there is no need to go to definition to find out what the parameter is. – garnet Feb 4 '18 at 14:40
9

Builder pattern does not solve anything for you and does not fix design failures.

If you have a class needing 10 parameters to be constructed, making a builder to construct it will not suddenly make your design better. You should opt for refactoring the class in question.

On the other hand, if you have a class, perhaps a simple DTO, where certain attributes of the class are optional, builder pattern could ease the construction of said object.

| improve this answer | |
1

Each part e.g. personal info, work info (each employment "stint"), each friend, etc., should be converted into their own objects.

Consider how you would implement an update functionality. The user wants to add a new work history. The user doesn't want to change any other parts of the information, nor previous work history - just keep them the same.

When there is a lot of information, building up the information one piece at a time is inevitable. You can group those pieces logically - based on human intuition (which makes it easier to use to a programmer), or based on usage pattern (what pieces of information are typically updated at the same time).

Builder pattern is just a way to capture a list (ordered or unordered) of typed arguments, which can then be passed into the actual constructor (or the constructor can read the builder's captured arguments).

The guideline of 4 is just a rule-of-thumb. There are situations which require more than 4 arguments and which there is no sane or logical way to group them. In those cases, it may make sense to create a struct which can be populated directly or with property setters. Notice that the struct and the builder in this case serve a very similar purpose.

In your example, however, you have already described what would be a logical way to group them. If you are facing a situation where this isn't true, maybe you can illustrate that with a different example.

| improve this answer | |

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.