Avoiding mutability is not that hard

Rodrigo Rearden
etermax technology
Published in
6 min readJun 26, 2020

--

How to easily implement immutability in C# and why we should all do it.

First of all, the problem: Side Effects

Side Effects are reads or writes of program state defined outside the scope of a function, or calls to another function that does so. Code units that do side effects outside of itself are, at best, coupled and untestable. Code units that side-effect inside itself are, at best, undefined behavior, at worst, bugs and chaos in productive code. As a side note, keep in mind that the definition of having side effects is recursive and viral: a side effecting method taints all its callers.

Side effects can occur outside of an object performing some operation, the program’s state gets changed outside the unit, for example, by performing some input/output. Side effects can also occur inside an object, by mutating its state. In this article, we will explore how to prevent objects from changing their own state.

This code has most of possible side effects in a few lines, and in every single method.

Mutability can cause… no, mutability causes bugs by leaving residual state inside an object (side effects). Then, when method is inadvertently called more than once, with the same parameters, it may yield different results, depending on the hidden mutation, becoming unpredictable.

Mutability also causes bugs, coupling, and semantic issues, by forcing the execution to an explicit and imperative order: first, mutate this, and then use the mutated value to calculate that. Every single line after a mutating instruction is now dependent on the former, and will not offer complete and fireproof information of what it is doing without reading all the lines above.

Mutable behavior is always unintended in Functional Programming, and usually implies that code is not testable in Test Driven Development. One may not be using Functional Programming but, for sure, one must be implementing through testing, say, using TDD.

Then, the solution: Immutable Objects

On the other hand, immutable code is always independent of the environment. One can always arrange it to just tell what it is doing for the parameters it receives in a declarative way by just naming functions, instead of going in an unending list of instructions on how it is doing it. It can easily be abstract about needless details.

Reading/writing state outside of an object is usually solved through dependency injection, or some implementation of the visitor pattern. By now, we’ve been fighting for decades against global variables and singletons, which is more or less covered by a lot of practices and developers. However, reading/writing state inside of an object (our test unit) is, by far, a more common issue at the moment, and here is where immutability comes up to make our code testable.

In short, the public enemy number one is using the assignment operator = for any purpose other than initializing a variable or field; as functional programmers call it: assignment is a destructive update. Thus we enter the realm of immutability, or, how to get rid of destructive updates, commonly spoken as “just make everything readonly”. But, of course, implementing immutable objects can be, and usually is, a verbose nuisance.

A code with mutations

Let’s start with a model of a typical bank account, and a simple use case: money transfer between two accounts. In this first implementation we just mutate the balance of both accounts:

A mutable Account type.

First steps

Well, a nice first step to achieve immutability would be to use a value type instead of a reference type, since value types are passed as parameters by value, we maintain immutability in any operation that uses our type, like Transfer:

A first approach to an immutable Account type.

We changed the type’s definition from a class to a struct, meaning that the type is now a value type. Mutations performed inside an instance method of a value type are immediately lost when the method exits, so, for those to persist, the method has to somehow communicate the changes performed. That immediately forces us to return twoAccount values from the Transfer operation: one for the sender account, one for the receiver account. The use of a tuple with both value objects is convenient here.

Cleaning up mutations in the implementation

The typing in this new code, ensures that callers of Transfer have to work with the fact that Account is an immutable value type, so, we’re immutable for the outside world. Mission accomplished!

However, inside our method, both instructions with the -= and += operators are clearly performing a destructive update in the values of sender and receiver instances, thus performing a side effect. We would also like to avoid that in our method’s code.

We create new value objects with the desired operations, instead of mutating fields.

By creating new objects with the changes to the balance, we now don’t need to use mutating operators. Notice that there already is a semantic improvement: the account balance calculation is not coupled to any specific detail on how the object is destructively updated, it is just Balance — amount and receiver.Balance + amount for each account.

We also remove the Balance property setter since we won’t need it anymore, preventing future changes in the code to inadvertently revert back to using mutation operations.

Let’s make it a bit more complex

What we have is quite readable now, but our types and operations are not always this simple. Let’s now add a couple more features, like a debt limit, and an address. For the sake of brevity, we’ll only show the relevant methods:

New features added to our value object, maintaining immutability.

With this new code, it becomes self evident that we are repeating ourselves quite a bit, every time we need to change a value, we explicitly create a new instance calling a constructor with every other field, particularly in the case of DoTransfer, we must also specify access to the receiver quite a few times.

Keeping it simple

One way to solve this is to refactor the creation of new Account instances using a couple C# tricks: default parameter values, optional values, named parameters, and the null coalesce operator, on an Update method.

For each field that can be updated, there is an optional parameter in Update. If the parameter is passed, then that will be the new value, if not, then the used value is the current instance’s one. Notice that fields that should not change, like owner are just passed as is.

We can now rewrite our methods in a more concise and semantic way, each method dealing only with the fields that they update and their new values:

The very same methods rewritten in a more succinct and clear way.

Conclusion

We explored the advantages of having immutable objects, how to implement them, and how to keep them simple. As said before, side effects are viral, and C# does not have any syntax or language mechanism to ensure immutability. The good news is that despite that, immutability prevents viral expansion. Each object that becomes immutable will not only push your side effects a bit more out of your domain logic, cleaning it up, but also forces you to be totally explicit about side-effecting, making your code clearer. I won’t give a code example of that right now… you’ll have to try it out yourself!

At the beginning of the article I made a statement about FP and TDD. Looking back, we started from an imperative approach, and ended up into a declarative one. This is what happens when you strive for immutability: your code becomes one-liners that strongly rely on composition to define behavior, instead of using instructions that define steps to be followed. Never mutating a data structure’s state, is actually the basis of Declarative Programming. If you add using functions as any other value type to that mixture, what you get is Functional Programming. So, in a way, I tricked you into getting closer to FP, without the need to go bragging about lambdas and pesky monads!

As a final line, here is the git repo with the complete code and the tests. Until next time!

--

--