Case classes and objects are one of the features of Scala that bring the most
benefit with the least added complexity. Having these value classes baked in the
syntax of the programming language brings many advantages: expressivity,
immutability, algebraic composition, equals
/hashCode
/toString
,
Serializable
for free…
Typically, I use case classes to model domain entities with strong guarantees by creating classes closely matching concepts and letting static typing and immutability making most of the invalid system states impossible to represent. Nesting case classes using sealed traits can let you model intricate domains.
But Scala type system cannot easily represent all the properties we might like to enforce and take for granted in the overall code base. Fortunately, there is an easy way to extend the guarantees of a case class thanks to its immutability: whatever property we enforce at the constructor will be an invariant.
Columbo was able to build a strong case like no one else.
Let’s use as example something as simple as a case class representing an even number.
The require
statement guarantees that all instances of Even
are actually
even.
Apart from relying on run-time exceptions, the main difference with type-enforced guarantees is that we are spending execution cycles to check the property on every object creation. When the check is expensive this simple solution might fall sort.
We can have more control over the case class initialization by making the constructor private as follows.
Now we can add factory methods enforcing an even number and provide invariant-preserving methods without any run-time check.
Note that we need to close one final loophole: the copy
method. Unless we
override it, we are exposed to having someone creating a valid object and then
an invalid copy of it.
We can use this pattern for other purposes like controlling how instances of a case class are created or hiding some other aspect of them in a module.