Value Objects

Publicado 03-03-2019

Un Value Object es un objeto que está caracterizado por sus valores en lugar de por su identidad. Esto quiere decir, que dos objetos con los mismos valores son equivalentes aún no siendo el mismo objeto, ni teniendo una identidad definida. Ejemplos clásicos de éstos son los rangos de valores, las fechas, las representaciones del dinero con la moneda correspondiente entre muchos otros.

El código fuente del video (separado en diferentes commits según el incremento) está disponible en Github para su consulta.

Características

Generalmente los Value Objects son inmutables; es decir, no cambian su valor una vez creados.

Esto tiene raíces en la concepción abstracta y matemática de los valores que obtienen las entidades. Un número “2”, que se incrementa en una unidad, no se transforma en un “3”, sino que genera un nuevo valor, totalmente distinto, que es un “3”. Si el “2” se convirtiera en un “3”, ahora siempre que pensemos en un “2”, estaríamos hablando de un “3”. Esto sería redefinir el valor de “2”, el cual es inmutable por definición.

En el mundo de la programación sabemos que la mutabilidad y el uso de referencias son dos ingredientes básicos para la introducción de errores. Cambiemos el estado de una objeto referenciado desde varios lugares en nuestro programa, y es probable que empecemos un efecto encadenado probablemente no deseado.

En la definición de Value Object establecimos que se caracteriza por sus atributos más que por su identidad. Es por ello que todo Value Object debería redefinir el método que verifica la igualdad, de acuerdo a esta premisa.

Un ejemplo clásico:

require "minitest/autorun"
require 'minitest/color'

class Money
    attr_reader :currency, :amount

    def initialize(currency, amount)
        @currency, @amount = currency, amount
    end
    def == other
        other.currency == @currency && other.amount == @amount
    end
    def add other
        raise "Incompatible currencies" if other.currency != @currency
        Money.new(@currency, other.amount + @amount)
    end
end

class MoneyTest < Minitest::Test
    def test_equals
        money1 = Money.new(:usd, 10)
        money2 = Money.new(:usd, 10)
        assert_equal money1, money2
    end

    def test_not_equals
        money1 = Money.new(:usd, 10)
        money2 = Money.new(:ars, 10)
        refute_equal money1, money2
    end

    def test_add
        money1 = Money.new(:usd, 10)
        money2 = Money.new(:usd, 15)
        money3 = money1.add(money2)
        assert_equal Money.new(:usd, 25), money3
    end

    def test_add_incompatible_currencies
        money1 = Money.new(:usd, 10)
        money2 = Money.new(:ars, 15)
        assert_raises do
            money1.add(money2)
        end
    end
end

En este fragmento de código vimos cómo se confecciona la clase Money, inmutable y que redefine la igualdad entre objetos de su tipo. Las pruebas que acompañan al código verifican que tanto la igualdad como la suma respeten los axiomas de la clase.

Ventajas

La primera ventaja que encontramos al introducir un Value Object es la de agrupar cierta lógica asociada a este nuevo objeto, con los datos de dicho objeto. Encapsulamos el comportamiento de una parte de la lógica de nuestro programa.

Generalmente la introducción de un Value Object permite simplificar el código, ya que está presente en más de un sitio, y puede extraerse la lógica que esté repetida a lo largo del código fuente.

Por último, un Value Object puede ser específico del dominio. Gracias a ello, permite modelar el lenguaje de objetos con el que estamos programando la solución al problema de software. En concreto, podemos llamar al objeto de acuerdo a nuestras necesidades, y asociar la lógica que creamos conveniente para el dominio puntual de nuestro problema.

Lectura profunda