HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Initializing immutable objects with a nested builder

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
objectsinitializingwithbuildernestedimmutable

Problem

I need to initialize several parameters of an object but I also wanted to make it immutable. At the same time I don't want the constructor to take all several parameters because some of them are optional and have default values that the user can change during initialization like an IReadOnlyDictionary so I was experimenting with different patterns.

I found out that there is an interesting fact that if you make the builder nested inside the actual immutable class it then has still access to its private setters. This means you can build the actual object using the actual object without having to create copies in the builder itself and it still is immutable after exporting it, isnt't it?

This is how it looks like:

class Foo
{
    private IDictionary _corge = 
        new Dictionary();

    public Foo() { }

    public string Bar { get; private set; }

    public IReadOnlyDictionary Corge => 
        (IReadOnlyDictionary)_corge;

    // API v3
    public static Foo Build(Action build)
    {
        return Builder.Build(build);
    }

    public class Builder
    {
        private Foo _foo = new Foo();

        private Builder() { }

        // API v1
        public static Builder Create()
        {
            return new Builder();
        }

        // API v2
        public static Foo Build(Action build)
        {
            var builder = new Builder();
            build(builder);
            return builder.ToFoo();
        }

        public Builder Bar(string bar)
        {
            _foo.Bar = bar;
            return this;
        }

        public Builder AddCorge(string key, string value)
        {
            _foo._corge.Add(key, value);
            return this;
        }

        public Foo ToFoo()
        {
            // prevents from reusing the builder after "ToFoo"
            var result = _foo;
            _foo = null;
            return result;
        }
    }
}


Usage with API v1:

```
var fooBuilder = Foo.Builder.Create();
fooBuilder.Bar(

Solution

When I think of immutable object I think of get only properties backed with readonly fields.


public string Bar { get; private set; }

That's not a fully immutable instance you still have the private set but removing the set completely will cause problems in your implementation, because your nested Builder class relies on the fact that it can set them.

What I think is better in this case is to have a private constructor to initalize your get only properties and instead of saving some private instance of the Foo object in the Builder you can save values :

public class Foo
{
    public string Bar { get; }

    public IDictionary Corge { get; }

    private Foo(string bar, IDictionary corge)
    {
        Bar = bar;
        Corge = corge;
    }

    public class Builder
    {
        private string _bar;
        private readonly IDictionary _corge = new Dictionary();

        public Builder Bar(string bar)
        {
            _bar = bar;
            return this;
        }

        public Builder AddCorge(string key, string value)
        {
            _corge.Add(key, value);
            return this;
        }

        public Foo Build()
        {
            return new Foo(_bar, _corge);
        }
    }
}


Usage :

Foo foo = new Foo.Builder().Bar("bar").AddCorge("key", "value").Build();


The action delegate doesn't quite fit in my solution so I just removed it.

You can add the public empty constructor if you want to. I personally wouldn't like to have static methods to create an immutable object, I prefer everything to be instance based.

One builder to rule them all..

One thing that I really don't like about your code is this

var fooBuilder = Foo.Builder.Create();
fooBuilder.Bar("baz");
var foo = fooBuilder.ToFoo();
fooBuilder.Bar("QUUX"); // bam!


You can't use the same builder to create multiple objects and it's not obvious that it will crash unless you check how the Builder class is implemented.

My solution fixes that

var fooBuilder = new Foo.Builder();
fooBuilder.Bar("baz");
var foo1 = fooBuilder.Build();
fooBuilder.Bar("QUUX"); // no bam!
var foo2 = fooBuilder.Build();

Code Snippets

public class Foo
{
    public string Bar { get; }

    public IDictionary<string, string> Corge { get; }

    private Foo(string bar, IDictionary<string, string> corge)
    {
        Bar = bar;
        Corge = corge;
    }

    public class Builder
    {
        private string _bar;
        private readonly IDictionary<string, string> _corge = new Dictionary<string, string>();

        public Builder Bar(string bar)
        {
            _bar = bar;
            return this;
        }

        public Builder AddCorge(string key, string value)
        {
            _corge.Add(key, value);
            return this;
        }

        public Foo Build()
        {
            return new Foo(_bar, _corge);
        }
    }
}
Foo foo = new Foo.Builder().Bar("bar").AddCorge("key", "value").Build();
var fooBuilder = Foo.Builder.Create();
fooBuilder.Bar("baz");
var foo = fooBuilder.ToFoo();
fooBuilder.Bar("QUUX"); // bam!
var fooBuilder = new Foo.Builder();
fooBuilder.Bar("baz");
var foo1 = fooBuilder.Build();
fooBuilder.Bar("QUUX"); // no bam!
var foo2 = fooBuilder.Build();

Context

StackExchange Code Review Q#133066, answer score: 7

Revisions (0)

No revisions yet.