4 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 5. Agregando funcionalidad al Enum

En el post anterior, os comentaba lo fácil que es agregar información al Enum. Aparte de contexto, podemos agregarle valores para darle funcionalidad.

Al final, agregando funcionalidades al mower orientation, necesitábamos agregar direccionalidad en los ejes de desplazamiento para saber si moverse debía sumar valores (N, E), o restar valores (S, W).

Los tests y la interacción con el objeto MowerPosition nos llevaron a este punto:

package mower.mower.domain.value_object

import mower.mower.domain.exception.InvalidOrientationException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.ValueSource
import java.util.stream.Stream
import kotlin.test.Test

private const val ORIENTATION: String = "N"
private const val INVALID_ORIENTATION: String = "H"

internal class MowerOrientationTest
{
    @Test
    fun `Should be build`() {
        val orientation = MowerOrientation.build(ORIENTATION)

        assertInstanceOf(MowerOrientation::class.java, orientation)
        assertThat(orientation.value).isEqualTo(ORIENTATION)
    }

    @ParameterizedTest(name = "{index} => orientation = ''{0}''")
    @ValueSource(strings = [ "N", "S", "E", "W" ])
    fun `Should be build with valid orientations`(value: String) {
        val orientation = MowerOrientation.build(value)

        assertThat(orientation.value).isEqualTo(value)
    }

    @Test
    fun `Should throw exception for invalid Orientation`() {
        assertThrows(InvalidOrientationException::class.java) {
            MowerOrientation.build(INVALID_ORIENTATION)
        }
    }

    @ParameterizedTest(name = "{index} => orientation = ''{0}'', movement = ''{1}'', result = ''{2}''")
    @MethodSource("orientationAndMovementProvider")
    fun `Should apply orientation movements`(orientationData: String, movementData: String, expectedOrientationData: String) {
        var orientation = MowerOrientation.build(orientationData)
        val movement = MowerMovement.build(movementData)
        val expectedOrientation = MowerOrientation.build(expectedOrientationData)

        orientation = orientation.changeOrientation(movement)

        assertThat(orientation.value).isEqualTo(expectedOrientation.value)
    }

    @ParameterizedTest
    @MethodSource("orientationAndYAxisAffectationProvider")
    fun `Should eval if affects Y axis`(orientationData: String, affectsYAxis: Boolean) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.affectsYAxis()).isEqualTo(affectsYAxis)
    }

    @ParameterizedTest
    @MethodSource("orientationAndXAxisAffectationProvider")
    fun `Should eval if affects X axis`(orientationData: String, affectsXAxis: Boolean) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.affectsXAxis()).isEqualTo(affectsXAxis)
    }

    @ParameterizedTest
    @MethodSource("orientationAndStepDirectionProvider")
    fun `Should eval direction step` (orientationData: String, expectedStepDirection: Int) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.stepMovement()).isEqualTo(expectedStepDirection)
    }

    companion object {
        @JvmStatic
        fun orientationAndMovementProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", "F", "N"),
                Arguments.arguments("N", "R", "E"),
                Arguments.arguments("E", "R", "S"),
                Arguments.arguments("S", "R", "W"),
                Arguments.arguments("W", "R", "N"),
                Arguments.arguments("N", "L", "W"),
                Arguments.arguments("W", "L", "S"),
                Arguments.arguments("S", "L", "E"),
                Arguments.arguments("E", "L", "N")
            )
        }

        @JvmStatic
        fun orientationAndYAxisAffectationProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", true),
                Arguments.arguments("S", true),
                Arguments.arguments("E", false),
                Arguments.arguments("W", false)
            )
        }

        @JvmStatic
        fun orientationAndXAxisAffectationProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", false),
                Arguments.arguments("S", false),
                Arguments.arguments("E", true),
                Arguments.arguments("W", true)
            )
        }

        @JvmStatic
        fun orientationAndStepDirectionProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", 1),
                Arguments.arguments("E", 1),
                Arguments.arguments("S", -1),
                Arguments.arguments("W", -1),
            )
        }
    }
}

La clase que lo implementa nos quedó así…

@JvmInline
value class MowerOrientation private constructor(val value: String) {
    private enum class Compass (val stepDirection: Int){
        N (POSITIVE_DIRECTION),
        E (POSITIVE_DIRECTION),
        S (NEGATIVE_DIRECTION),
        W (NEGATIVE_DIRECTION)
    }

    init {
        try {
            Compass.valueOf(value)
        } catch (exception: IllegalArgumentException) {
            throw InvalidOrientationException.withValues(value, Compass.values().contentToString())
        }
    }

    companion object {
        private const val COMPASS_STEP: Int = 1
        private const val POSITIVE_DIRECTION: Int = 1
        private const val NEGATIVE_DIRECTION: Int = -1

        @JvmStatic
        fun build(value: String): MowerOrientation
        {
            return MowerOrientation(value)
        }
    }

    fun changeOrientation(mowerMovement: MowerMovement): MowerOrientation {
        val currentCompass = Compass.valueOf(value)

        if (mowerMovement.isClockWise()) {
            val futureCompass = Compass.values().getOrElse(currentCompass.ordinal + COMPASS_STEP) { Compass.N }
            return MowerOrientation(futureCompass.name)
        }

        if (mowerMovement.isCounterClockWise()) {
            val futureCompass = Compass.values().getOrElse(currentCompass.ordinal - COMPASS_STEP) { Compass.W }
            return MowerOrientation(futureCompass.name)
        }

        return this
    }

    fun affectsYAxis(): Boolean {
        return Compass.N.name == value || Compass.S.name == value
    }

    fun affectsXAxis(): Boolean {
        return Compass.E.name == value || Compass.W.name == value
    }

    fun stepMovement(): Int {
        return Compass.valueOf(value).stepDirection
    }
}

Posible refactor

Podríamos agregarle caraterísticas al Enum para simplificar las funciones affectsYAxis, affectsXAxis de la misma forma que está articulado con la función stepMovement. Dejando la clase con estos cambios.

...

@JvmInline
value class MowerOrientation private constructor(val value: String) {
    private enum class Compass (val stepDirection: Int, val affectsXAxis: Boolean, val affectsYAxis: Boolean){
        N(POSITIVE_DIRECTION, NOT_X_AXIS, Y_AXIS),
        E(POSITIVE_DIRECTION, X_AXIS, NOT_Y_AXIS),
        S(NEGATIVE_DIRECTION, NOT_X_AXIS, Y_AXIS),
        W(NEGATIVE_DIRECTION, X_AXIS, NOT_Y_AXIS)
    }
...
    companion object {
...
        private const val NOT_X_AXIS: Boolean = false
        private const val NOT_Y_AXIS: Boolean = false
        private const val X_AXIS: Boolean = true
        private const val Y_AXIS: Boolean = true
...
    }
...
    fun affectsYAxis(): Boolean {
        return Compass.valueOf(value).affectsYAxis
    }

    fun affectsXAxis(): Boolean {
        return Compass.valueOf(value).affectsXAxis
    }

    fun stepMovement(): Int {
        return Compass.valueOf(value).stepDirection
    }
}

comments powered by Disqus