Refined Types in Domain Driven Design

In a world of domain driven design we like to use lots of types. In particular, we use a lot of value types, and we will even choose to create a type which is little more than a wrapper for a single value of an underlying type. Often the set of valid values of the new type is not much more than a strict subset of the set of valid values of the underlying type.

There’s a number of benefits of this approach. Very simply, it helps to read and reason about the code and it highlights the core domain logic. A more complex benefit is that it makes the logic more testable by isolating handling of edge cases to those parts of the code which are most closely associated with the definition of that type.

As an example, we might look at a port number. Variables holding port numbers are often just modelled as an integral type, such as Int. But in reality the set of valid port numbers is a much smaller than the set of valid integers. Port numbers can’t be negative and they can’t be more than 65355, whereas on the JVM an Int can be any value from -2 billion to +2 billion. If we want to really represent the range of port numbers we might prefer to say that a port number is an integer in the range of 0 to 65535 inclusive.

In code this might look like

case class PortNumber private (num: Int)

object PortNumber {
  def apply(num: Int): Either[String, PortNumber] = num match {
    case i: Int if i < 0 => Left("Port number may not be negative")
    case i: Int if i > 65535 => Left("Port number may not be more than 65535")
    case _ => Right(new PortNumber(num)
  }
}

What we’ve done here is to encapsulate the case class constructor so that it can’t be instantiated other than via the companion object’s apply factory method. The apply factory then performs some validation, and returns an Either containing an error message or a valid value of type PortNumber. We also rely on the immutability of case classes to ensure that the underlying value cannot be changed, and the class can’t be subclassed so there are no complicated inheritance interactions to worry about. Now, anywhere in the rest of the code where a variable of type PortNumber appears we can be reasonably sure the the variable contains a valid value.

The cost of the type safety is the complexity associated with writing all these constructors, and chaining their apply factory results together to combine multiple value types into more complex value types. There’s also a slight problem around accessing the underlying value as it’s now a field within the case class instance rather than being the instance itself. However, this pattern is generally very valuable and so it’s usually worth the slightly more complex interactions around creating instances of them.

What would be ideal would be a way to create one of these types quickly and easily.

Refined Types

From the Haskell world we have a concept called Refined Types. Refined types are an implementation of the idea that you can define a new type (e.g. PortNumber) as being some kind of ‘refinement’ of another type (e.g. Int). To define the new type we attach a predicate to the old type which allows us to define the subset of allowed values.

In Scala the refined library is an implementation of the Haskell concept. Now we can define our PortNumber much more simply…

import eu.timepit.refined.W
import eu.timepit.refined.{refineMV, refineV}
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval

type PortRange = Interval.Closed[W.0.T, W.65535.T]
type PortNumber = Int Refined PortRange

val portValidated: Either[String, PortNumber] = refineV[PortRange](8080)
val port: PortNumber = refineMV[PortRange](8080)

Here we define our predicate PortRange as being a closed interval over all the valid values a port number can have, and then we define PortNumber as being a normal Int refined using that predicate. The library then gives us two factory methods, paramaterised on predicate, which allow us to construct instances of the refined type. One method produces an Either of a error message or the refined value – good for validating inputs from untrusted sources and handling them safely and cleanly. The other method throws an exception if the input value is invalid – good for producing values from constants or in test scenarios where data can be trusted and exceptions can be treated as test failures.

Given this basic core infrastructure for creating domain value types, there are a plethora of libraries to help with integrating them with other frameworks or libraries. For example, the play-refined library gives mechanisms to help with conversion to and from values used in controller arguments and to or from JSON.


Posted

in

by