Immutability

Strive to make your classes as immutable as possible.

The OOP community learnt this lesson much earlier than we tend to think : Mutability makes the code harder to understand.

Immutable Code

Have you heard of the Gilded Rose kata by Terry Hughes ? It's a perfect illustration of why mutability must be avoided as much as possible, and self-contained deep and far away from us.

Please, take a seat, breath deeply, and look at this code carefully.

export class Item {
  name: string;
  sellIn: number;
  quality: number;

  constructor(name, sellIn, quality) {
    this.name = name;
    this.sellIn = sellIn;
    this.quality = quality;
  }
}

export class GildedRose {
  items: Array<Item>;

  constructor(items = [] as Array<Item>) {
    this.items = items;
  }

  updateQuality() {
    for (let i = 0; i < this.items.length; i++) {
      if (this.items[i].name != 'Aged Brie' && this.items[i].name != 'Backstage passes to a TAFKAL80ETC concert') {
        if (this.items[i].quality > 0) {
          if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
            this.items[i].quality = this.items[i].quality - 1
          }
        }
      } else {
        if (this.items[i].quality < 50) {
          this.items[i].quality = this.items[i].quality + 1
          if (this.items[i].name == 'Backstage passes to a TAFKAL80ETC concert') {
            if (this.items[i].sellIn < 11) {
              if (this.items[i].quality < 50) {
                this.items[i].quality = this.items[i].quality + 1
              }
            }
            if (this.items[i].sellIn < 6) {
              if (this.items[i].quality < 50) {
                this.items[i].quality = this.items[i].quality + 1
              }
            }
          }
        }
      }
      if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
        this.items[i].sellIn = this.items[i].sellIn - 1;
      }
      if (this.items[i].sellIn < 0) {
        if (this.items[i].name != 'Aged Brie') {
          if (this.items[i].name != 'Backstage passes to a TAFKAL80ETC concert') {
            if (this.items[i].quality > 0) {
              if (this.items[i].name != 'Sulfuras, Hand of Ragnaros') {
                this.items[i].quality = this.items[i].quality - 1
              }
            }
          } else {
            this.items[i].quality = this.items[i].quality - this.items[i].quality
          }
        } else {
          if (this.items[i].quality < 50) {
            this.items[i].quality = this.items[i].quality + 1
          }
        }
      }
    }

    return this.items;
  }
}

Do you understand the mechanisms behind that code ?

What are the business rules ?

What with these magic strings and magic numbers ?

Why 50, 11 or 6 ?

If you're not familiar with the exercise, you're probably lost and may as well give up.

The main problem of that code, other than the fact that's it is not object-oriented, is its mutability.

The quality and the sellIn property of the items are constantly changing, making it all the more difficult to comprehend.

The item object is having an identity crisis. It's not even an object, it's a data-structure.

Here, the solution isn't to make Item immutable, though it can be one potential way of solving this problem.

The exercise forbid changing the Item class anyway, it's part of the difficulty.

The solution is to instead capture the intent (namely, updating the quality of a product) and grouping the mutations in the end. That's the whole purpose of the exercise.

Below an example of solution.

export class GildedRose {
  items: Array<Item>;

  constructor(items = [] as Array<Item>) {
    this.items = items;
  }

  updateQuality() {
    for (let i = 0; i < this.items.length; i++) {
      this.updateOneItemQuality(this.items[i]);
    }

    return this.items;
  }

  updateOneItemQuality(item: Item) {
    if (item.name === 'Sulfuras, Hand of Ragnaros') {
      return;
    }

    const delta = this.qualityUpdate(item);

    // These are the only mutations in the class :)
    item.sellIn--;
    item.quality = this.clamp(item, delta)
  }

  private qualityUpdate(item: Item) {
    if (item.name === 'Aged Brie') {
      return this.agedBrie(item);
    } else if (item.name === 'Backstage passes to a TAFKAL80ETC concert') {
      return this.backstagePass(item);
    } else {
      return this.commonItem(item);
    }
  }
  
  private commonItem(item: Item) {
    return item.sellIn > 0 ? -1 : -2;
  }

  private agedBrie(item: Item) {
    return item.sellIn > 0 ? 1 : 2;
  }

  private backstagePass(item: Item) {
    if (item.sellIn === 0) return -item.quality;
    if (item.sellIn < 6) return 3;
    if (item.sellIn < 11) return 2;
    return 1;
  }

  private clamp(item: Item, delta: number) {
    return Math.min(50, Math.max(0, item.quality + delta));
  }
}
When methods are names instead of verbs, you either have getters or objects waiting to appear.

When methods are names instead of verbs, you either have getters or objects waiting to appear.

See how much more clearer the code is ? We got rid of all these nested loops and mutations and checks and extracted the real intent of the code inside well named functions and tiny refactoring steps (just the refactoring would warrant a full article by itself).

But the most important point is the immutability. We grouped the change at a single spot of the function : the end. The change in quality is represented inside a delta, and we perform the mutation at a single place.

In the absence of switch expressions we must rely on extracting the pattern matching code inside a function named qualityUpdate. If your language supports a form of pattern matching, consider using it instead of creating another function.

Immutable Objects

If there's one thing we learnt from many decades of computer science and software engineering, it's that mutability is the source of many problems. It makes it very difficult to track the evolution of an object and to work with multiple threads.

But most importantly, immutability is much easier to reason with.

Think about it.

1 + 2

This little program computes the result 3.

But should this computation change the value of 1 ?

Should 1 become 3 ?

We could get philosophical, but we can both agree that changing the value of a number would be terribly disturbing.

Same goes for objects. Or is it ?

One huge clash between immutability and object-oriented programming is statefulness. In the purest OO paradigm, objects maintain boundaries between their internal world and their external world. From the outside, calling the methods and invoking the services of an object may and will probably alter the state of that object.

Imagine publishing posts or creating an account into system. Doing so would definitely change the state of the system.

So is OO incompatible with Immutability ?

Not so much, because OO is a rather wide term, and not every objects of our program can be considered a system.

I find that some objects are a better fit for immutability than others.

Take Value Object. It's a pattern in which a value is encapsulated into an object to enrich its meaning and capabilities.

Every Value Object is immutable by definition. Two Value Objects are the same if their internal state are equivalent (their bit representation is the same). And most value objects have arithmetic operations.

Think of a Seconds class that represent a time delay in seconds.

export class Seconds {
  constructor(private value: number) {
    if (value < 0) {
      throw new Error('Seconds cannot be negative');
    }
  }

  static empty() {
    return new Seconds(0);
  }

  static milliseconds(value: number) {
    return new Seconds(Math.floor(value / 1000));
  }

  static minutes(value: number) {
    return new Seconds(value * 60);
  }

  static hours(value: number) {
    return new Seconds(value * 60 * 60);
  }

  static days(value: number) {
    return new Seconds(value * 60 * 60 * 24);
  }

  equals(other: Seconds): boolean {
    return this.value === other.value;
  }

  isLessThan(other: Seconds): boolean {
    return this.value < other.value;
  }

  difference(other: Seconds): Seconds {
    return new Seconds(Math.abs(this.value - other.value));
  }

  asHours() {
    return Math.round(this.value / 60 / 60);
  }

  asMinutes() {
    return Math.round(this.value / 60);
  }
}

Every operation creates a new instance, and that's perfectly logical. Adding seconds should not change the meaning of any of the operands, it should yield another value.

So I found that an object is a perfect fit for immutability when either :

  • Two objects can be considered equal simply by comparing their bit representation

  • The object has some form of arithmetic

Long-lived objects such as Entities aren't a good fit for immutability because they carry a persistent lifecycle. Changing the name of the user shouldn't yield another user. It's the same person, but with a different name.

Last updated