Value-Based Classes and Domain Modelling in Kotlin

Ahmed Mourad
The Startup
Published in
4 min readSep 9, 2020
By whatwolf at freepik.com

Not to be confused with value types of project Valhalla.

Value-based classes, as defined in the Java docs, are classes which conform to a set of rules:

- are final and immutable (though may contain references to mutable objects);

- have implementations of equals, hashCode, and toString which are computed solely from the instance's state and not from its identity or the state of any other object or variable;

- make no use of identity-sensitive operations such as reference equality (==) between instances, identity hash code of instances, or synchronization on an instances's intrinsic lock;

- are considered equal solely based on equals(), not based on reference equality (==);

- do not have accessible constructors, but are instead instantiated through factory methods which make no committment as to the identity of returned instances;

- are freely substitutable when equal, meaning that interchanging any two instances x and y that are equal according to equals() in any computation or method invocation should produce no visible change in behavior.

Regarding the first rule: Immutable classes cannot contain references to mutable objects, because they’re immutable. The rest of the article assumes they don’t.

In Kotlin, that may look like this:

Basic value-based class

There are three advantages of this:

  • You are certain that all instances of Password are “good passwords”, it becomes an invariant of your system.
  • Your domain model enforces its own rules, which is more convenient than having the “api consumer” enforce them.
  • You have complete control over the instance your factory returns.

Pretty simple, huh? In fact, too simple.

By freepik at freepik.com

What does returning null here mean to the api consumer? That this’s a bad password? But why?

Returning null in this case is barely helpful. What do we tell the app user? bad password? better luck next time? What if the api user wants to execute different actions on different failing scenarios? We need to provide more context!

Which takes us to a pretty controversial subject…

Exceptions vs ADTs

There are mainly two ways in which this problem is tackled:

  • Exceptions:
Value-based class with exceptions

This looks nice at first, but maintaining it, definitely ain’t!

All the try-catching aside, what happens when we decide we need to add seventy-five more constraints on our user’s passwords? We add the exceptions, update our documentations and .. start asking who has been instantiating our class?

Some people take this a step further and throw exceptions directly inside the constructors which is even more dangerous, but more on that later.

Overall, we can do better!

By freepik at freepik.com
  • Algebraic Data Types:
Value-based class with ADTs

By using sum types (Either), sealed classes and exhaustive when, the compiler is now responsible for maintaining our code and will force us (unless explicitly told not to) to handle all the error scenarios wherever we instantiate this class.

Further more, your “constructor” is now an honset function, it returns exactly what its signature says it would, it doesn’t hijack your program’s execution, which is a huge plus for readability and maintainability.

It also allows us to do something like this, which we couldn’t do with exceptions:

Value-based class with ADTs and violations list

Neat! Isn’t it?

Our code is now predictable and our invariants are established!

Except, they’re not…

By Kaentian Street at shutterstock.com

This is valid Kotlin code:

Uh-Oh

The copy method of Kotlin’s data classes breaks our invariants, it can bypass all of our rules and it can create “illegal” instances of our class.

Even worse for those throwing exceptions inside the constructor! Who would expect this innocent copy method to throw an exception? No one, and you can’t document it for that matter!

It is a well-known design flaw in the language, with no apparent way to fix it, while keeping backward-compatibility.

So .. dead end? Was this all in vain? That would’ve been funny, but no!

This’s where the NoCopy compiler plugin comes into play, by simply annotating our data class with @NoCopy:

Value-based class with ADTs, violations list and NoCopy

The copy method of this data class can no longer be referenced or called:

Not this time!

With that taken care of, our invariants are now truly established and our code is now truly safe.

By danmir12 at freepik.com

Summary

By using a combination of value-based classes and ADTs, we managed to create robust data models that enforce their own rules and have the compiler help us maintaining them.

We also used the NoCopy compiler plugin to prevent the copy method of the data classes from breaking our invariants.

Links

For ready implementations of Either and other functional constructs,
Arrow-kt is considered the best choice for Kotlin developers.

Follow me on Twitter & Github, Connect on LinkedIn.

Thank you for reading this article through. Liked it? Clap your 👏 to say “thanks!” and help others find this article.

--

--