11

In Java and C#, you can create an object with properties that can be set at initialisation by either defining a constructor with parameters, defining each property after constructing the object, or using the builder/fluid interface pattern. However, C# 3 introduced object and collection initialisers, which meant the builder pattern was largely useless. In a language without initialisers, one could implement a builder then use it like so:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

Conversely, in C# one could write:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

...eliminating the need for a builder as seen in the previous example.

Based on these examples, are builders still useful in C#, or have they been superseded entirely by initialisers?

svbnet
  • 221
  • 2
  • 5
  • 2
    `are builders useful` why do I keep seeing these sorts of questions? A Builder is a design pattern - sure, it may be exposed as a language feature but at the end of the day it's just a design pattern. A design pattern _cannot_ be "wrong". It may or may not fulfil your use case but that doesn't make the entire pattern wrong, just the application of it. Remember that at the end of the day design patterns solve particular problems - if you don't face the problem, then why would you try solving it? – VLAZ Oct 01 '16 at 10:27
  • @vlaz I'm not saying builders are wrong - in fact my question was asking if there were any use cases for builders given initialisers are an implementation of the most common case where you would use builders. Obviously, people have answered that a builder is still useful for setting private fields, which in turn has answered my question. – svbnet Oct 01 '16 at 10:45
  • 1
    You were saying that a design pattern is not useful. Which implies it's wrong. Again, this only relies on the assumption that a design pattern should always solve your problem _regardless_ of what that problem is. – VLAZ Oct 01 '16 at 10:55
  • 3
    @vlaz To clarify: I am saying that initialisers have _largely replaced_ the "traditional" builder/fluid interface pattern of writing an inner builder class, then writing chaining setter methods inside that class which create a new instance of that parent class. I am _not_ saying that initialisers are a replacement for that specific builder pattern; I am saying that initialisers save developers having to implement that specific builder pattern, _save for_ the use cases mentioned in the answers, which _does not_ make the builder pattern useless. – svbnet Oct 01 '16 at 11:10
  • 1
    So...you have answered your own question then. Use builders when you need to, don't use them when they aren't needed due to better alternatives. – VLAZ Oct 01 '16 at 11:14
  • 6
    @vlaz I don't understand your concern. "You were saying that a design pattern is not useful" - no, Joe was *asking if* a design pattern is useful. How is that a bad, wrong, or mistaken question? Do you think the answer is obvious? I don't think the answer is obvious; this seems like a good question to me. – Tanner Swett Oct 01 '16 at 14:30
  • @TannerSwett If I were to ask you "why are you useful" would you take that as just a question or would you think I might be saying you aren't useful? Because a question worded like that usually means the latter. And the answer is indeed obvious, OP has already derived it here in the comments without needing for it to be spelled out. Moreover, any number of resources will show how and why a given design pattern is used and what it solves. Should we also start asking the same for _any_ of them _any_ time we find a situation they don't apply? – VLAZ Oct 01 '16 at 15:12
  • 2
    @vlaz, the singleton and service locator patterns are wrong. Ergo design patterns can be wrong. – David Arno Oct 03 '16 at 15:13
  • Initializers have largely replaced **single assignment per method** builders. Not builders whose methods do more than a blind assignment. – Flater Jul 08 '23 at 04:28

3 Answers3

12

Object initializers require that the properties must be accessible by the calling code. Nested builders may access private members of the class.

If you wanted to make Vehicle immutable (via making all setters private), then the nested builder could be used to set the private variables.

user248215
  • 129
  • 4
12

As touched on by @user248215 the real issue is immutability. The reason you would use a builder in C# would be to maintain the explicitness of an initializer without having to expose settable properties. It is not a question of encapsulation which is why I have written my own answer. Encapsulation is fairly orthogonal as invoking a setter does not imply what the setter actually does or tie you to its implementation.

The next version of C#, 9.0, is likely to introduce a with keyword which will allow for immutable objects to be initialized clearly and concisely without needing to write builders.

Another interesting thing you can do with builders, as opposed to initializers, is that they can result in different types of objects depending on the sequence of methods called.

For example

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

*Just to clarify the example above, it is a pattern matching library that uses the builder pattern to build up a "match" by specifying cases. Cases are appended by calling the Case method passing it a function. If value is assignable to the function's parameter type it is invoked. You can find the full source code on GitHub and, since XML comments are hard to read in plain text, here are some unit tests that show it in action.

Aluan Haddad
  • 678
  • 4
  • 9
  • I don't see how this couldn't be done an object initialiser, using a `IEnumerable>` member. – Caleth Sep 11 '17 at 08:48
  • @Caleth That was actually something I experimented with but there are some issues with that approach. It doesn't allow for conditional clauses (not shown but used and demonstrated in the linked documentation) and it does not allow for type inference of `TResult`. Ultimately type-inference had a lot to do with it. I also wanted it to "look" like a control structure. Also I didn't want to use an initializer since that would allow mutation. – Aluan Haddad Sep 11 '17 at 15:02
  • `init;` already exists in C# so you can use initializers but not mutate the state post initialization. – Flater Jul 08 '23 at 04:29
  • @Flater indeed it does, it also now has manifest expresses precisely what I wrote my pattern matching for, so there are a number of things which are obsolete in a sense about the details of this answer. However, the reason I think the pattern matching Builder API remains a good example is that it demonstrates so many different ways in which builders, especially as they can adjust their parameter and return types to express a workflow, indicates strongly how the builder pattern remains a useful abstraction. – Aluan Haddad Jul 08 '23 at 06:09
1

They all serve different purposes!!!

Constructors can initialize fields that are marked readonly as well as private and protected members. However, you are somewhat limited in what you can do inside a constructor; you should avoid passing this to any external methods, for example, and you should avoid calling virtual members, since they may execute in the context of a derived class that hasn't constructed itself yet. Also, constructors are guaranteed to run (unless the caller is doing something very unusual), so if you set fields in the constructor, code in the rest of the class can assume that those fields are not going to be null.

Initializers run after construction. They only let you call public properties; private and/or readonly fields cannot be set. By convention, the act of setting a property should be pretty limited, e.g. it should be idempotent, and have limited side effects.

Builder methods are actual methods and therefore allow more than one argument, and by convention can have side effects including object creation. While they cannot set readonly fields, they can pretty much do anything else. Also, methods can be implemented as extension methods (like pretty much all of the LINQ functionality).

John Wu
  • 26,032
  • 10
  • 63
  • 84