6 minute read

Índice de temario

Día Tema a tratar
1 Definiendo objetivos
2 Proyecto inicial y value objects
3 Tratando con excepciones
4 Validando con Enum y parametrized test
5 Ampliando uso del Enum
6 Entidades

Día 2. Proyecto inicial y Value objects

Primer obstáculo, buscar y comprender el equivalente de Composer en Kotlin. En este caso tuve la suerte de contar con el IDE de Jetbrains (la versión community es suficiente) que te facilita la vida creando un proyecto de consola desde cero. En mi caso, tuve que usar la versión 16 de java por incompatibilidades con la 17.

Creando proyecto kotlin

Hasta aquí bien. Ahora empieza lo bueno…


1. Poner el README del mower kata para definir el objetivo

Como dijimos, poner la primera piedra significa empezar por los tests usando DDD como elemento estructural. En Java y Kotlin, por herencia del primero, la carpeta main y test están al mismo nivel dentro de src.

Vamos a crear dentro de main/kotlin nuestro bounded context mower y módulo mower.

“Naming convention rule” de java. Las carpetas (o packages como se suele llamar) en minúscula.


2. Mirar cómo crear un Value Object

En este caso vamos a mirar cómo crear la pieza más pequeña del universo de DDD. El value object.

Un Value Object debe cumplir con las siguientes especificaciones.

Requisitos
Debe contener un primitivo u otros value objects
Debe ser inmutable
Debe poder validarse a sí mismo al ser construido
Debe poder ser construido de varias formas si es necesario “named constructors”

En PHP, un Value Object podría tener este aspecto.

final class YCoordinate
{
    private const MINIMUM_AXIS_VALUE = 0;

    /**
     * @throws YCoordinateOutOfBoundsException
     */
    private function __construct(private int $value)
    {
        if (self::MINIMUM_AXIS_VALUE > $this->value) {
            throw YCoordinateOutOfBoundsException::withCoordinate($this->value);
        }
    }

    public static function build(int $value): self
    {
        return new self($value);
    }

    public function value(): int
    {
        return $this->value;
    }
}

Esta clase cumple con los estándares de un Value Object. Cabe remarcar que soy un acérrimo defensor de usar named constructors. Aportan contexto y cuando un objeto puede ser creado de diferentes formas, haber creado un primer named constructor luego nos abre las puertas a agregar un segundo o tercero centrándonos en el contexto de porqué se usó ese y no otro de los que puede tener el objeto.

Ahora veamos si un Value Object en Kotlin se puede crear de forma similar con las mismas normas.

Mirando las especificaciones del lenguaje, parece que el tema del named constructor es diferente. Nos lo tendremos que currar un poco…

Como dijimos anteriormente… antes de escribir cualquier cosa, escribimos tests con nuestros objetivos para el value object.

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import kotlin.test.Test

private const val Y_POSITION: Int = 3
private const val INVALID_POSITION: Int = -1

internal class YMowerPositionTest
{
    @Test
    fun `Should be build` ()
    {
        var yMowerPosition = YMowerPosition.build(Y_POSITION)

        assertInstanceOf(YMowerPosition::class.java, yMowerPosition)
        assertThat(yMowerPosition.value).isEqualTo(Y_POSITION)
    }

    @Test
    fun `Should throw exception for invalid values` ()
    {
        assertThrows(Exception::class.java) {
            YMowerPosition.build(INVALID_POSITION)
        }
    }
}

Usando los data class

Los data class son prometedores… con muy poco código tienes casi todo lo que necesitamos, aunque hay un temilla con los named constructors que no me convence.

Podemos hacerlos inmutables declarando los valores con “val” en lugar de “var”, soportan el “copy”, “equals”, “toString” de forma simple y llana sin hacer nada por nuestra parte… y acceder a los valores es fácil… peeeeeero no podemos hacer privado el constructor original, ya que el “copy” necesita tener acceso a dicho constructor… y eso crearía la puerta hacia un constructor sin contexto, que es justamente lo que queremos evitar.

Aunque hubiera estado bien tener todo eso por defecto, sólo por no poder hacer privado el constructor original, queda descartado. Aunque todavía se pueden usar como DTOs.

private const val MINIMUM_AXIS_VALUE: Int = 0

data class YMowerPosition constructor (val value: Int) {

    init {
        if(value < MINIMUM_AXIS_VALUE){
            throw Exception("Invalid value")
        }
    }
}
Data class como Value Object Cumple Comentarios
Debe contener un primitivo u otros value objects -
Debe ser inmutable -
Debe poder validarse a sí mismo al ser construido -
Debe poder usarse named constructors No podemos hacer privado el constructor primario

Usando las clases normales con “companion object”

Con los companion object en una clase estándar podemos cumplir el objetivo de los named constructors.

class YMowerPosition private constructor (val value: Int) {

    init {
        if(value < MINIMUM_AXIS_VALUE){
            throw Exception("Invalid value")
        }
    }

    @JvmStatic
    companion object {
        private const val MINIMUM_AXIS_VALUE: Int = 0

        fun build(value: Int): YMowerPosition {
            return YMowerPosition(value)
        }
    }
}
Class con Companion object como Value Object Cumple
Debe contener un primitivo u otros value objects
Debe ser inmutable
Debe poder validarse a sí mismo al ser construido
Debe poder usarse named constructors

Os preguntaréis porqué no hay un accessor para “value”. Simple, al poder declarar una propiedad como inmutable con “val”, podemos dejar que sea pública, ya que no se puede volver a cambiar.


Optimizando el Value Object con Inline class

Si vamos a albergar primitivos, Kotlin sacó el concepto de los Inline classes.

Su uso está pensado para optimizar recursos y desecha el concepto de identidad implícito en un objeto a nivel de instancia. Al declarar una clase como “value class”, básicamente sólo deja comparar valores con el operador ==.

Con lo que nuestro value object puede retocarse un poco…

@JvmInline
value class YMowerPosition private constructor (val value: Int) {
...
}

Y los test seguirán pasando.

¡¡¡Los inline class tienen un límite!!!

Sólo son aptos para albergar un valor usando su constructor, con lo que si tu Value Object tiene más valores al inicializarse, deberá ser una clase normal.


Bonus track sobre las constantes y las funciones estáticas

Las constantes pueden estar a nivel de companion object o a nivel de fichero fuera de la clase. En mi caso prefiero dentro de la clase. Hay otras formas de conseguir algo parecido, pero sin usar la palabra clave const y no lo veo lógico. También, fijaos que las constantes se pueden declarar con un tipo de datos.

Las funciones estáticas, para poder ser llamadas desde java, tienen que llevar la anotación @JvmStatic. Si no, java no es capaz de utilizarlo de forma cómoda.

class C {
    companion object {
        @JvmStatic fun callStatic() {}
        fun callNonStatic() {}
    }
}

C.callStatic(); // works fine
C.callNonStatic(); // error: not a static method
C.Companion.callStatic(); // instance method remains
C.Companion.callNonStatic(); // the only way it works


Naming conventions

https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html

https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html

https://www.thoughtco.com/using-java-naming-conventions-2034199

Data classes

https://kotlinlang.org/docs/data-classes.html

Inline classes

https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md#inline-classes-are-user-defined-value-classes

https://kotlinlang.org/docs/inline-classes.html

Constructors and validation

https://kotlinlang.org/docs/classes.html#constructors

https://kotlinlang.org/docs/classes.html#companion-objects

Constants

https://kotlinlang.org/docs/basic-types.html#literal-constants

https://kotlinlang.org/docs/properties.html#compile-time-constants

Static functions

https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-methods


comments powered by Disqus