loading...

Kotlin and Domain-Driven Design—Value Objects

flbenz profile image Florian Benz Updated on ・7 min read

Does the programming language matter when pushing for domain-driven design (DDD)? Yes. Does any programming language prevent the usage of domain-driven design? None of the major languages does. Why? Placing the primary focus on the domain is possible with nearly all languages, but some languages make using constructs and patterns advocated for domain-driven design more pleasant to use. Kotlin is one of those languages. Note that domain-driven design is not only about the implementation but also about the process.

Value objects are a core concept of DDD. They contain attributes but no identity. Money is a common example. It's a value if it's just about the amount and currency. A banknote, in contrast, has a unique ID and thus an identity.

Why use Value Objects?

Value objects provide a lot of advantages. They make the domain explicit, e.g. by using Money as a wrapper instead of just two fields of type BigDecimal and String. One can go deeper and wrap the BigDecimal as Amount and the String as CurrencyUnit. Readers of the code will understand it's about money and maintainers will be unable to pass something not representing money. In addition, the wrapper can contain validations and thus additional domain knowledge and also increase security. Restricting an ID object to 22 characters or even a pattern tells the reader something about the structure, catches errors earlier and also prevents the ID from holding arbitrary malicious content.

Getting started with Value Objects

So how does Kotlin help? Kotlin makes creating value objects an enjoyable experience. It just feels natural to write something like:

data class SomeId(
    val value: String
) {
    override fun toString(): String = value
}

Calling the field value avoids the odd-looking someObject.id.id. Overriding toString() allows to just use someObject.id when adding the ID to exceptions are log messages. A new experimental feature in Kotlin 1.3 makes value objects with a single attribute even shorter and more performant:

inline class SomeId(
    val value: String
)

An inline class removes the overhead of the wrapper, i.e. the object instantiation, by replacing all usages by the wrapped type. From a developers point of view, it's the same as the data class above, but on the bytecode level, it is optimized.

With static typing like in Kotlin, the compiler helps to prevent bugs and IDEs can offer better guidance. For example, a method taking two IDs could look like foo(userId: String, productId: String) and thus there is a chance of mixing up IDs but foo(userId: UserId, productId: ProductId) prevents mix-ups.

Validating Value Objects

Validation can be added to ensure only valid value objects are created. This has the advantage that if you receive an instance of such a value object, you know it's valid. The validation can be limited to the constructors and the init block if the value object is immutable. A value object is immutable if all fields are values (val) and all field types are also immutable. Kotlin helps because a lot of the standard structures are immutable. Lists are a good example. A Kotlin List is immutable and mutability has to be explicit via MutableList. The following example shows an ID consisting of exactly 10 lower case characters or digits.

val pattern = "[a-z0-9]{10}".toRegex()

data class SomeId(
    val value: String
) {
    init {
        require(pattern.matches(value)) {
            "The ID $value does not match the pattern $pattern."
        }
    }
}

It's impossible to inject SELECT * FROM users or <script> into a SomeId. By using and validating value objects, one does not only catch errors early on but also improves security. It's also valuable information about the domain.

JUnit 5 comes with explicit support for Kotlin and makes testing validations straight forward:

@Test
fun `should not allow the creation of invalid IDs`() {
    assertThrows<IllegalArgumentException> {
        SomeId("tooshort")
    }
}

Immutable Value Objects

Value objects don't have to be immutable but immutability comes with a lot of advantages. Immutable values can freely be passed around. There is no need to create copies. Even concurrent access is safe without modifications. Testing is simplified to creating different instances via the constructor and then asserting the returned results of properties and methods. Immutable objects also encourage side-effect free functions and those come with their own set of advantages.

Value objects are the perfect fit for functionality corresponding to the value, for example, calculations that are purely based on attributes of a single value object. Where would this functionality live without a value object? In some service or some utility class? This might not be the best location.

Operator Overloading

Kotlin offers operator overloading and this can be beneficial for value objects. Just think of an Amount value object offering similar functionality as BigDecimal, e.g. + for adding up two values and returning a new instance as the result. One can also work around the fact that just because compareTo tells us two BigDecimal values represent the same number, equals can tell us the internal representation is different and thus yields false.

data class Amount(
    val value: BigDecimal
) : Comparable<Amount> {

    override operator fun compareTo(other: Amount): Int = 
        this.value.compareTo(other.value)

    override fun equals(other: Any?): Boolean {
        if (other == null) {
            return false
        }
        return this.compareTo(other as Amount) == 0
    }

    override fun hashCode(): Int {
        return value.multiply(BigDecimal("100")).intValueExact()
    }

    operator fun unaryMinus(): Amount = Amount(this.value.negate())

    operator fun plus(b: Amount) = Amount(this.value + b.value)

    operator fun minus(b: Amount) = Amount(this.value - b.value)
}

Unit tests should be sufficient to test value objects as they should only depend on other value objects and their functions should have no side effects. With JUnit 5 and AssertJ, one test case for the Amount object can look as follows:

@Test
fun `should implement infix plus operator`() {
    // given
    val a = Amount(BigDecimal("10.25"))
    val b = Amount(BigDecimal("9.11"))
    // when
    val result = a + b
    // then
    assertThat(result).isEqualTo(Amount(BigDecimal("19.36")))
}

Companion Objects

Static factory methods are a good option when the creation process involves more than just passing values. Placing the methods in the companion object of the value object helps to keep all the logic together. Continuing on the amount example, we could have the following static factory method:

companion object {
    fun scaleToTwoDecimals(value: BigDecimal) =  
        Amount(value.setScale(2, RoundingMode.HALF_EVEN))
}

Testing is again straight forward:

assertThat(Amount.scaleToTwoDecimals(BigDecimal("1.235")))
    .isEqualTo(Amount(BigDecimal("1.24")))

Factory methods placed in the companion object are easy to find — either by looking at the code or via the auto-completion of IDEs. However, companion objects are not a fit for the factory method pattern where an interface is defined for creating an object, but the subclasses decide which class to instantiate.

Extensions

Kotlin provides the ability to extend existing types. Strings can be extended with a method creating SomeId objects:

fun String.toSomeId() = SomeId(this)

In this case, the extension and the constructor yield the same value object:

assertThat("0123456789".toSomeId()).isEqualTo(SomeId("0123456789"))

Here, it's a matter of taste of whether to use the constructor or the extension function.

Sealed Classes

Kotlin offers support for algebraic data types. An algebraic data type is a type formed by combining other types. There are two common classes of algebraic data types: product types and sum types. Product types are represented by a list of fields —a composition of types by AND. Sum types are represented by enums or sealed classes, i.e. types from a limited set —a composition of types by OR. The main benefit of sealed classes is the closed hierarchy and thus no else clause is needed in when expressions.

Algebraic data types help to make impossible states impossible, i.e. by using the type system to not even allow invalid states.

Let's look at an example: A CSS color can be presented in several ways. Here, we limit ourselves to three representations: names like LightGreen, hex values like #ADD8E6, and RGB values like rgb(10, 156, 246). A simple representation could look as follows:

data class CssColor(
    val name: CssColorName?,
    val hexValue: HexColor?,
    val red: RgbComponentValue?,
    val green: RgbComponentValue?,
    val blue: RgbComponentValue?
)

One could improve a bit on the design by moving RGB to a subtype but either way, a lot of invalid states can be constructed: all fields being null or a name and a hex value are set at the same time. Sealed classes allow us to design a CSS color where all invalid states are impossible:

sealed class CssColor {
    data class Named(val name: CssColorName) : CssColor()
    data class Hex(val hex: HexColor) : CssColor()
    data class Rgb(
        val red: RgbComponentValue,
        val green: RgbComponentValue,
        val blue: RgbComponentValue) : CssColor()
}

Pattern matching over a closed hierarchy works nicely:

val cssRepresentation = when(color) {
    is CssColor.Named -> color.name
    is CssColor.Hex -> "#${color.hex}"
    is CssColor.Rgb -> "rgb(${color.red}, ${color.green}, ${color.blue})"
}

Type-Safe Builders

There are lots of ways to construct a CssColor with and without helper functions. The plain creation without any helpers for an RGB color can look like this:

CssColor.Rgb(
    red = RgbComponentValue(10),
    green = RgbComponentValue(20),
    blue = RgbComponentValue(30)
)

One could leave out the named arguments but named arguments improve readability and also help if ever a field is added or removed. Wouldn't it be nicer to just write:

color {
    rgb(red = 10, green = 20, blue = 30)
}

By using well-named functions in combination with function literals with receiver it is possible to create type-safe, statically-typed builders in Kotlin. This allows the creation of whole domain-specific languages (DSLs), for example, for CSS. A bit of code is needed to enable the DSL above:

class CssColorFactory {
    fun rgb(red: Int, green: Int, blue: Int) = 
        CssColor.Rgb(
            red = RgbComponentValue(red),
            green = RgbComponentValue(green),
            blue = RgbComponentValue(blue)
        )
}

fun color(init: CssColorFactory.() -> CssColor): CssColor {
    return CssColorFactory().init()
}

Conclusion

Kotlin makes working with value objects fun. A common complaint from Java developers is that wrapping and especially mapping values adds too much boilerplate. I rarely hear this from Kotlin developers. But no matter the language, overusing value objects outside of the core domain can be overkill. Also, note that DDD is not a fit everwhere. Our experience with DDD in cases where we have a lot of business logic is very positive: discussions are more focused, new joiners pick up the code and logic quicker, and the burden to add features months after the initial implementation is lower.

References

Discussion

pic
Editor guide
Collapse
lsolano profile image
Lorenzo Solano Martínez

Good article @flbenz ,

Just a minor issue (form)

  • Typo: creatingSomeId (Extensions section) is then implemented on the code sample as fun String.toSomeId() = SomeId(this) the names does not match.
Collapse
flbenz profile image
Florian Benz Author

Thank you, I fixed it.