Posted in ddd, c#

Handling immutable types (part 2)

You can read the first post of the series - Handling immutable types part 1 - If you haven't already.


In my last post, I went on to explain some of the benefits of immutable types in your data structures. Which is great, but what happens when your types start to become unwieldy?

An object, which requires a lot of parameters, can be difficult to build. Sure there is nothing stopping you on a technical front having an object taking a dozen parameters, but it sure makes for messy code.

Let's take our Person object from part 1.

public class Person
{
    public int Id { get; private set; }
    public string FirstName { get; private set; }
    public string MiddleName { get; private set; }
    public string LastName { get; private set; }
    public string Email { get; private set; }
    public Address Address { get; private set; }
    public int Age { get; private set; }
    public bool Married  { get; private set; }
    public bool Homeowner { get; private set; }
    public string Occupation { get; private set; }
    public decimal AnnualIncome { get; private set; }
    public int Children { get; private set; }
}

You can imagine this object could easily be bigger (or smaller of course). To make this type immutable, we would give this class a public constructor, and pass in any mandatory values. Great, but that would mean we potentially have a constructor with 11 parameters (not including id). That looks ugly when you have to pass all those arguments into a method.

What we really want is a way to expose our parameters publically, but keep our object immutable.
If we could have public properties we can take advantage of object initializers, even use packages such as AutoMapper.

We can use the builder pattern

We can employ the builder pattern on large, unwieldy immutable types - to build us an instance.

We do this by creating a nested Builder class in our Person object.

public sealed class Builder : Builder<Person>
{
    private readonly Person _instance = new Person();

    protected override Person GetInstance()
    {
        // validation here...
        if (string.IsNullOrWhiteSpace(FirstName))
            throw new ArgumentNullException();

        return _instance;
    }

    public string FirstName
    { 
        get { return _instance.FirstName; }
        set { _instance.FirstName = value; }
    }

    // .. Other manditory arguments.

Person only exposes a private or protected constructor. So the only way we can build a new Person is through the builder.

Person person = new Person.Builder
                {
                    FirstName = "Marc"
                    // ...
                };

Now, lets take a look at the Builder<T> class.

public abstract class Builder<T>
{
    public static implicit operator T(Builder<T> builder)
    {
        return builder.Build();
    }

    private bool _built;

    public T Build()
    {
        if(_built)
            throw new InvalidOperationException("Instance already built");

        _built = true;

        return GetInstance();
    }

    protected abstract T GetInstance();
}

To save us having to call Build() ourselves on the builder, we will use an implicit operator which will call it for us and return T directly.

Conclusion

What we gain:

  • We have kept immutability of our types.
  • We have lost the big constructor signatures. Making how we get a new instance more flexible.
  • We can leverage object initializers and tools such as AutoMapper.

It's not without it's drawbacks of course:

  • This solution currently doesn't cater for a type having multiple constructor overloads. It is perfectly possible, but would require more work from the builder.
  • By convention, we are making all public properties of the Builder mandatory to create a new Person, however we lose compile time checking (something that you would get with value type parameters). If we left FirstName unassigned; it's not going to complain until runtime when it tries to build a Person and throws an ArgumentNullException.

Overall I think this is quite an elegant solution to large, unwieldy immutable types.