Functional C#: Immutability

The topic described in this article is a part of my Applying Functional Principles in C# Pluralsight course.

I’m starting a series of articles in which I want to show how to program in C# in a more functional way.

Why immutability?

The biggest problem of enterprise software development is code complexity. Code readability is probably the first goal you should try to achieve on your way to building a software project. Without it, you are not able to make a qualified judgment about the correctness of your software or at least your ability to reason about it is significantly reduced.

Do mutable objects increase or reduce code readability? Let’s look at an example:

// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
 
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
 
// Adjust criteria if nothing found
if (customers.Count == 0)
    AdjustSearchCriteria(queryObject, name);
 
// Is queryObject changed here?
Search(queryObject);

Has the query object been changed by the time we search customers for the second time? Maybe yes. But maybe not. It depends on whether or not we found anything for the first time and on whether or not AdjustSearchCriteria method changed the criteria. To find out what exactly happened, we need to look at the AdjustSearchCriteria method code. We can’t know it for sure by just looking at the method signature.

Now compare it to the following code:

// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
 
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
 
if (customers.Count == 0)
{
    // Adjust criteria if nothing found
    QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name);
    Search(newQueryObject);
}

It is now clear that AdjustSearchCriteria method creates new criteria that are used to perform a new search.

So, what are the problems with mutable data structures?

  • It is hard to reason about the code if you don’t know for sure whether or not your data is changed.

  • It is hard to follow the flow if you need to look not only at the method itself, but also at the methods it calls.

  • If you are building a multithreaded application, following and debugging the code becomes even harder.

How to build immutable types

If you have a relatively simple class, you should always consider making it immutable. This rule of thumb correlates with the notion of Value Objects: value objects are simple and easily made immutable.

So how do we build immutable types? Let’s take an example. Let’s say we have a class named ProductPile representing a bunch of products we have for sale:

public class ProductPile
{
    public string ProductName { get; set; }
    public int Amount { get; set; }
    public decimal Price { get; set; }
}

To make it immutable, we need to mark its properties as read-only and create a constructor:

public class ProductPile
{
    public string ProductName { get; private set; }
    public int Amount { get; private set; }
    public decimal Price { get; private set; }
 
    public ProductPile(string productName, int amount, decimal price)
    {
        Contracts.Require(!string.IsNullOrWhiteSpace(productName));
        Contracts.Require(amount >= 0);
        Contracts.Require(price > 0);
 
        ProductName = productName;
        Amount = amount;
        Price = price;
    }
}

Let’s say we need to reduce the product amount by one when we sell one of the items. Instead of changing the existing object we need to create a new one based on the current:

public class ProductPile
{
    public string ProductName { get; private set; }
    public int Amount { get; private set; }
    public decimal Price { get; private set; }
 
    public ProductPile(string productName, int amount, decimal price)
    {
        Contracts.Require(!string.IsNullOrWhiteSpace(productName));
        Contracts.Require(amount >= 0);
        Contracts.Require(price > 0);
 
        ProductName = productName;
        Amount = amount;
        Price = price;
    }
 
    public ProductPileSubtractOne()
    {
        return new ProductPile(ProductName, Amount - 1, Price);
    }
}

So what do we get here?

  • With immutable class, we need to validate its code contracts only once, in the constructor.

  • We absolutely sure that objects are always in a correct state.

  • Objects are automatically thread-safe.

  • The code’s readability is increased as there’s no need to step into the methods for making sure they don’t change anything.

What are the limitations?

Of course, everything comes at a price. While small and simple classes benefit from immutability the most, such approach is not always applicable to larger ones.

First of all, there are performance issues attached. If your object is quite big, necessity to create a copy of it with every single change may hit the performance of your application.

A good example here is immutable collections. Their authors took into account potential performance problems and added Builder class that allows to mutate the collection. After the preparation is done, you can finalize it converting to an immutable collection:

var builder = ImmutableList.CreateBuilder<string>();
builder.Add("1");                                   // Adds item to the existing object
ImmutableList<string> list = builder.ToImmutable();
ImmutableList<string> list2 = list.Add("2");        // Creates a new object with 2 items

Another issue is that some classes are inherently mutable and trying to make them immutable brings more problems than solves.

But don’t let these issues keep you from creating immutable data types. Consider pros and cons of every design decision and always take common sense into account.

Summary

In most cases, you will be able to benefit from immutability, especially when you keep your classes small and simple.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus