Immutable design is not just a patch
to some traditional design approach, like requesting a new object on every change. That's stupid. That doesn't work.
Immutable design is a separate strategy. I will design a few models now,
both ways, and compare the results. You might be astonished at how much
the immutable mindset differs from the traditional object-oriented sense of
design, which is based on mutable objects. Here is the simplest of all examples - a class
that models a book, only containing the title. This class is immutable.
Behind the scenes, it defines a public read-only property with an init-only
setter, populated from the primary constructor. You don't need these in C#.
The record will do it all behind the scenes. How about a mutable counterpart?
In mutable classes, the state can change. That rings some alarm bells, and
so we hold the state private. Mutation is possible, and even welcome.
But it must pass through the validation gate. Initializing or mutating methods
also clean any incoming data. There must be a valid state at the outset,
hence the need for a validating constructor. But this is so embarrassing.
The compiler cannot see I am setting the field to a non-null value every time.
Let me refactor it a bit to plug all the holes. Both the primary constructor and the
property setter are now passing through the same validation for the title.
Immutable design removes a part of this cumbersome code from your classes.
A few lines less may not sound important, but a bug should definitely attract your attention.
The next request is to support keywords associated with each book.
With a caveat. The request is also to include
words from the book's title. Can you spot the bug already?
I will add the same property getter to the immutable class, and while I
do that, you will tell me: Where is the bug? Well, it is definitely not
in this immutable class. But the mutable variant is broken.
I can change the title through the public setter later, but the object would
retain keywords from the original title. This happens when mutable state
components affect each other. Managing mutable state suddenly
becomes complex and error-prone. We do not observe those
difficulties in an immutable design. The mutation causes the
recalculation of the entire state. And it is only going to be worse.
Adding new keywords at the back of the mixed list of keywords will
certainly make this situation worse. Let me fix this bug before implementing
the same feature in the immutable design. I am separating the external
keywords from the title keywords. Then, I will fix the bug in the title setter.
I should pull this into a helper method to avoid code duplication.
This should have fixed all the issues with the mutable class.
But my implementation is already growing complex. That is a severe issue for a class
that still has no behavior at all. Adding any behavior will only make it longer.
This method analyzes the private, encapsulated state to produce a result.
We encapsulate the state primarily because we must control the mutation.
Do you see the problem with this method? Mutable classes protect the state and expose
public operations that encapsulate algorithms. And algorithms are business-specific.
This Boolean expression is certainly not the only possibility when comparing phrases.
But before I fix this issue, let me give you some coupons for video courses on Dometrain.
Dometrain is an online courses platform run by fellow content creator Nick Chapsas, and
it is the go-to place for up-to-date courses on modern software engineering topics.
From testing to clean architecture and modular monoliths all the way to DDD,
microservices, Docker and Kubernetes, Dometrain has a rich collection of courses that
will help you get up to speed with each topic by expert authors who have been practising
what they teach in real companies for years. Dometrain just launched Dometrain Pro,
a subscription offering that gives access to all of the platform’s courses.
With more than 24.000 satisfied students, you can’t go wrong with Dometrain, and
the first 100 of you can use code ZORAN, to get 15% off any course.
Now, back to the code. There are two principal ways to avoid the
pitfall of varying algorithms in mutable design. One is to turn the class
abstract and let a derived class implement a concrete comparison algorithm.
This approach quickly leads to inheritance hell, all too well known to object-oriented programmers.
A more flexible solution is to accept a strategy that applies to each keyword and
let some other class implement it. If I may twist your brain, this is the pure
functional implementation of that same method. Where is the search phrase? It's
in the closure, don't worry. This ends the demo segment where I
was struggling with mutable design. The two principal problems in mutable
design are complexity and bugs. You have seen both in this class.
I will now implement the same features in the immutable class.
The complete source code for this demo is significantly larger
than what I will display here. The source code is available
to patrons of this channel. Let me emphasize that every registered
patron helps hundreds of other programmers watch these videos for free,
so please join me on Patreon and help me maintain this free channel.
I will now apply the immutable design. Forget about keywords at first
and only focus on the title. How shall I use this class?
Instantiating the class only gives you the immutable instance.
You can ask it questions, apply functions to it, but not change its state.
C# supports with expressions for records, which copy all pieces of state
except those you explicitly set anew. However, I lost support for with expressions on
the title when I removed the init-only setter. And I did that to enforce validation. Why?
Because I still have that personality disorder called primitive obsession, which I
developed while designing the mutable model. This mutable model is a
remnant of the old days when state encapsulation was the highest principle.
The entire class is obsessed with primitive types. Now, back to the immutable design, which should
favor domain-specific types over primitive ones. Why not give title its own definition, wrap that
string, validate it in a trivial type, and then use it with complete confidence everywhere else?
Look at the book definition now. With title being a domain type, the book
can finally focus on its own domain rules. Titles will fight their
own battles in this design. Maybe expose a static factory function
and ensure that all consumers use it. Maybe turn it into a smart constructor,
avoiding needless exceptions. In a proper functional design, this
static factory function would return an optional title or a result type.
Back to using the immutable objects. Substituting a new title object
means we obtain a new book object. Beware of the bang operator.
This is not the proper use of it. I have only cut a corner because
dealing with negative paths is not the topic of this demo; that is all.
Working with immutable objects substantially differs from what we would do in mutable design.
Here, I am setting the new title in place. How could that turn into a bug? - tell me.
I am printing the book's title and a dashed line under it.
This is the corresponding operation on the immutable model.
When I run the demo, they produce the same output. Did you spot the bug?
Let me show you. Imagine a call to a function in the middle.
The function operates on an immutable book. The second function will do the same
thing, only this time to the mutable book. And now I have the bug.
Running the demo will show it. The line under the mutable
book is not measured well. That is the consequence of sharing a mutable
reference between the caller and the function. This particular situation
is called the aliasing bug. Alias is the ancient name used in
computer science for a shared variable, when an object comes under two names.
The immutable variant, however, cannot contain an aliasing bug.
The function would need to communicate the modified object explicitly.
There are always two of them, one before and one after the change.
And here is how immutable design prevents repeating that bug.
If I used the new object in the printout, then I should notice a mismatched
dashed line in the rest of the printout. I don't see another solution
but to prepare a brand-new line. Making an aliasing bug becomes
very difficult this time. Running the demo will show how the immutable
variant utilizes the modified object without the aliasing bug observable in the mutable demo.
This completes another stage in this demonstration: working with immutable objects.
The next topic is to add support for collections in an immutable class.
I will remind you that you can download the completed source code, with a few details
added compared to what you can see in this video. The source code is available on my Patreon page.
I want to implement support for those same keywords I had in the mutable class.
Adding a list to an immutable class is nonsense. That list would be a shared mutable
reference, bringing the aliasing bugs back into the game, big time.
I have just instantiated a list containing a keyword and then
used it to construct a Book object. The aliasing bug may manifest if I
continue adding items to this list. How on earth will the book instance
react to this change if it is immutable? Therefore, mutable collections are out
of the question in immutable classes. Immutable classes usually
depend on immutable collections. If this is the first time you have
seen an immutable list, go and learn about immutable collections right now.
An immutable collection would return a new one whenever you try mutating it.
This call returns a new ImmutableList, which differs from the existing one in that
it contains one additional string at its back. The AddKeyword method must return the new
instance with the modified keywords list. Many programmers freak out when they see a
new object created, fearing it might be slow. Somebody would have noticed performance
issues already if they were significant. For example, in a database application, this
operation would be three orders of magnitude faster than a single call to a database.
Also, the with expression only makes a shallow copy, which is blazingly fast.
If we move back to the consumer, we will see that the aliasing bug is now
impossible to make - it simply doesn't compile. I can put an empty immutable list here.
That would give me an intermediate Book instance. Then, add the keyword to
obtain the final instance. Alternatively, if there is already a list object,
convert it to an immutable list and detach the Book object from that mutable list.
The subsequent modification to the list is pointless.
The immutable Book class has made any aliasing bugs impossible.
The last remaining feature is to add filtering of books by a search phrase.
The request is to combine the book's keywords with those extracted from the title.
With the title being an individual type, it can take part of that duty to itself.
I think you should really start to enjoy the simplicity of immutable
designs if you haven't already. I will now make a change to the Book class.
The external keywords will come into a separate, private collection.
The publicly visible keywords will be a sequence, and I will construct that
sequence by concatenating the title's and the book's keywords into one sequence.
Where is that search and compare method I had in the mutable class?
Well... go to the place where it belongs, and you will probably find it there.
The incredible power of immutable designs is that classes are finally free
to expose their components publicly. I could not do that in the mutable
design because of the mutation! Someone would scramble my object's state.
Let me give you a hint of what that would look like.
This class could be part of a library that specializes in matching
books against criteria of all kinds. One algorithm is just to recognize
a search phrase in the keywords. Another is to allow the keyword
to be longer than the phrase. The third algorithm goes the other way,
letting the phrase contain an entire keyword. Yet another algorithm would apply an
elaborate calculation of the likeness between a search phrase and a keyword and
then take the maximum across all keywords. Public components make immutable classes
highly susceptible to extension methods and pattern-matching expressions.
Both are out of reach for most of the mutable classes.
What you see here would be four derived classes in a traditional mutable design.
In an immutable design, it requires no modifications to the subject class.
It will remain simple forever after. You can now step on learning how to design
proper, unbreakable immutable types. Learn about the power of functional types and
start writing incredibly short and effective code.