How to Ensure Internal Consistency of Mutable and Immutable Objects

by Zoran Horvat

Motivation

Internal consistency is fundamental requirement for all objects. Without consistency guarantee, all clients of an object would have to defend from unpredictable circumstances whenever they try to do anything. In consistent world, client calls an operation on an object like this:

obj.DoSomethingForMe();

But if client is uncertain about object's status, it must plug all the holes:

if (obj.AreYouFine())
    obj.DoSomethingForMe();
else
    this.GoBellyUp();

This piece of code has two fundamental flaws. First, it tests object's status explicitly - something that heavily relies on programmer's consciousness. Poorly informed coder, or just a sleepy one, could easily forget to test the object before accessing it. Second flaw is that the code has a negative branch which is activated when subordinate object is not fine. This leaves the ultimate consumer of this code in a very unpleasant situation because there is no certainty that the operation is going to succeed at any given time. Even worse, we cannot say whether the desired operation is going to be done, or is it going to explode back into our face.

Bottom line is that any object we reference must be fine at all times. Object with incomplete content, missing references, mismatched internal values, etc. is inconceivable.

Enforcing Consistency in Objects

We can generally divide all objects by their ability to change own internal state. Those objects that do not change after they have been initialized are referred to as immutable objects. On the opposite end there are mutable objects, which expose operations that change their internal state.These two kinds of objects are kept consistent in different ways.

Consistency in Immutable Objects

Example of immutable objects are instances of the System.String class. Once a string is initialized, it doesn't allow changes to its underlying buffer. Any operation that sounds like changing the string actually instantiates another string object and returns it as a result. For example, Insert method lets us insert one string into another at specific position. It does so by creating another string with appropriate content:

string a = "sing";
string b = a.Insert("ometh", 1);
// b: "something"

Constructor is the only place where internal state of an immutable object can be set. The question how to ensure consistency of such an object then points in constructor's direction. Constructor is in charge to validate its arguments and to make sure that object is properly and fully initialized from them.

As a practical demonstration, consider a class which represents an item in a purchase receipt:

public class PurchaseItem
{

    public string UPC { get; private set; }

    public decimal UnitCost { get; private set; }

    public int Quantity { get; private set; }

    public decimal TotalCost
    {
        get
        {
            return this.UnitCost * this.Quantity;
        }
    }

    public PurchaseItem(string upc, decimal unitCost, int quantity)
    {

        if (string.IsNullOrEmpty(upc)
            || unitCost <= 0.0M
            || quantity <= 0)
            throw new ArgumentException();

        this.UPC = upc;
        this.UnitCost = unitCost;
        this.Quantity = quantity;

    }

}

This class is immutable. Product code (UPC), unit cost and quantity purchased cannot be changed after initialization. Now suppose that we have a requirement to let the user change quantity before the receipt is printed. That can easily be done in brigh tradition of immutable objects - by creating a new object from the existing one:

public class PurchaseItem
{

    ...

    public PurchaseItem ChangeQuantity(int quantity)
    {
         return new PurchaseItem(this.UPC, this.UnitCost, quantity);
    }

}

Observe that in this method we have not checked whether quantity passed to the method is positive or not. That validation is performed by the constructor when new object is initialized. Current object really doesn't care about validity of this argument. It is the new object responsible to make itself valid or to fail to initialize if consistency rules have been violated.

Consistency in Mutable Objects

Objects that are allowed to change after initialization still have to be consistent. This means that their constructor is responsible for validation none the less. But after the initialization, client is free to call some operations that modify this established consistent state. Those operations must ensure that consistency is preserved over state changes.

Take a purchase receipt as an example. Receipt class will aggregate a set of purchased items. Its responsibilities will be in the domain of manipulating the items and providing basic aggregation services, such as summing up item costs. Here is the first look at the Receipt class:

public class Receipt
{

    private IList<PurchaseItem> Items;

    public Receipt()
    {
        this.Items = new List<PurchaseItem>();
    }

    public void AddItem(PurchaseItem item)
    {

        if (item == null)
            throw new ArgumentNullException("item");

        AddItemInternal(item);

    }

    public PurchaseItem RemoveItem(string upc)
    {

        if (string.IsNullOrEmpty(upc))
            throw new ArgumentException();

        PurchaseItem item =
            this
            .Items
            .Where(i => i.UPC == upc)
            .Single(); // may throw exception

        this.Items.Remove(item);

        return item;

    }

    public decimal TotalCost
    {
        get
        {
            return this.Items.Sum(item => item.TotalCost);
        }
    }

    private void AddItemInternal(PurchaseItem item)
    {
        if (ContainsUpc(item.UPC))
            AddQuantityToItem(item.UPC, item.Quantity);
        else
            AddNewItem(item);
    }

    private bool ContainsUpc(string upc)
    {
        return this.Items.Any(item => item.UPC == upc);
    }

    private void AddQuantityToItem(string upc, int quantity)
    {
        PurchaseItem item = RemoveItem(upc);
        item = item.ChangeQuantity(item.Quantity + quantity);
        AddNewItem(item);
    }

    private void AddNewItem(PurchaseItem item)
    {
        this.Items.Add(item);
    }

}

Observe how we had to make many tests before any operation was done which alters internal state of the object. Actually, most of the Receipt class code is dedicated to preserving internal consistency of the object. That goal is far from trivial to achieve. Therefore, before deciding to let the object be mutable, consider cost of that decision. In many cases it is not worth the price. Instead of trying to predict all issues that may arise while changing internal state of the object, it might be quite sufficient to let the final state be specified through the constructor and then seal the object for changes, i.e. make it immutable.

Conclusion

In this article we have demonstrated two ways in which objects cope with consistency issues. One is simpler and it is fully implemented in the constructor. The other is more complicated and it copes with state transformations.

In practice, it is easier to write immutable objects because with them we do not have to consider all possible state changes. That saves many lines of code and reduces chance that we have forgot some viable scenario. Only if it is absolutely necessary shall we design a mutable class. And in that case, we must ensure that all valid transitions are covered with logic which protects object from corruption.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub