Made Tech Blog

An Argument for Immutable Class Design

If you write code you write bugs. It is a fact of life as a developer. The more code you write, the more complicated it gets and the more likely it will contain bugs. Consider this logic:

If you want to introduce fewer bugs, write less code.

What does this have to do with immutable class design I hear you ask? Well, there are many arguments for immutable designs. There are arguments that immutable code is easier and faster to test. There are arguments that immutable code is easier to use in distributed and threaded systems. There are arguments that immutable deployments can keep us from building snowflakes and enforce a scalable mindset from the start. I am not presenting any of these arguments today, instead I am presenting a much simpler argument. My argument today is:

Writing immutable code leads to a reduction in lines of code.

In a recent code dojo here at the Made offices, one colleague suggested a mutable object design to which I offered an alternative, immutable design. Rather than derailing the dojo with an explanation I will now outline my thoughts.

The dojo itself was charged with building an anagram grouper. Given a file of words we had to produce an array of anagram arrays. The first step was to provide words to the class without loading from a file and producing an array of grouped anagrams.

The mutable class design would require each word to be added to an AnagramGrouper object with #add_word and then a #group method being called to produce the output.

class AnagramGrouper
  def initialize
    @words = []
  end

  def add_word(word)
    @words << word
  end

  def group
    # logic to group anagrams
  end
end

This design means an AnagramGrouper represents one session of grouping. That is, it maintains an array of words to be grouped, so given a different set of words you would need to create a new instance of the grouper.

grouper = AnagramGrouper.new
grouper.add_word('dog')
grouper.add_word('god')
grouper.add_word('bob')
grouper.group # => [['dog', 'god'], ['bob']]

grouper = AnagramGrouper.new
grouper.add_word('debit card')
grouper.add_word('bad credit')
grouper.group # => [['debit card', 'bad credit']]

As you see here this design has lead to verbose usage. We could of course load all words at construct time:

class AnagramGrouper
  def initialize(words)
    @words = words
  end

  def group
    # logic to group anagrams
  end
end

Which then leads to a shortening of usage code:

grouper = AnagramGrouper.new(['dog', 'god', 'bob'])
grouper.group # => [['dog', 'god'], ['bob']]

grouper = AnagramGrouper.new(['debit card', 'bad credit'])
grouper.group # => [['debit card', 'bad credit']]

However we could go one step further, let’s turn this design into an immutable one.

class AnagramGrouper
  def group(words)
    # logic to group anagrams
  end
end

Now the usage is super simple and only requires one instance of AnagramGrouper.

group = AnagramGrouper.new
grouper.group(['dog', 'god', 'bob']) # => [['dog', 'god'], ['bob']]
grouper.group(['debit card', 'bad credit']) # => [['debit card', 'bad credit']]

So not only did an immutable class design lead to a reduction of code in the class but also a simpler interface and therefore simpler usage.

This is a very superficial argument for immutability, but I think it’s a powerful one. As developers we all too often lean on complicated object design when a simpler design would have been easier and quicker to implement.

The mutable design stored state in an instance variable @words, but why? What benefit does it provide to be able to build up an array of words? What benefit does it provide to require a new AnagramGrouper instance every time you want to group a separate set of words? Why store state when you don’t have to?

My final question I leave you with is, why write code that didn’t need to be written?

About the Author

Avatar for Luke Morton

Luke Morton

Chief Technology Officer at Made Tech