Introducción
El patrón (más bien mini-patrón) que vamos a ver hoy es ampliamente usado en Domain-Driven-Design (DDD). El patrón Value Object refuerza un concepto en ocasiones muy olvidado de los principios de orientación a objetos, especialmente por aquellos que estamos habituados a lenguajes débilmente tipados: la encapsulación.
Existen varias definiciones de lo que un Value Object (VO) es, tenemos por ejemplo la de Ward Cunningham:
Examples of value objects are things like numbers, dates, monies and strings. Usually, they are small objects which are used quite widely. Their identity is based on their state rather than on their object identity. This way, you can have multiple copies of the same conceptual value object. Every $5 note has its own identity (thanks to its serial number), but the cash economy relies on every $5 note having the same value as every other $5 note.
La de Martin Fowler:
When programming, I often find it’s useful to represent things as a compound. A 2D coordinate consists of an x value and y value. An amount of money consists of a number and a currency. A date range consists of start and end dates, which themselves can be compounds of year, month, and day.
(…) Objects that are equal due to the value of their properties, in this case their x and y coordinates, are called value objects.
O la de Carlos Buenosvinos, Keyvan Akbary y Christian Soronellas:
Value Objects are a fundamental building block of Domain-Driven Design, and they’re used to model concepts of your Ubiquitous Language in code.
Básicamente y dicho de otra forma: si comparamos nuestro sistema con un ser vivo, los Value Object equivaldrían a células, si lo comparamos con un edificio, serían los ladrillos.
Un Value Object se usa para modelar conceptos de nuestro sistema, como pueden ser identificadores, fechas o rangos de fechas, precios, pesos, velocidades (prácticamente cualquier magnitud es modelable como VO), o incluso títulos, nombres o direcciones. Las entidades, por ejemplo, se componen de Value Objects.
Características
Existen una serie de características distintivas que hacen que una clase sea considerado un Value Object.
Mide, cuantifica o describe un concepto
Los VO están concebidos para que midan, cuantifiquen o describan un concepto de nuestra capa de dominio. No se consideran algo, sino que son un valor, y como tal tienen un fin.
Inmutabilidad
Los VO se conciben como objetos inmutables. Dado su limitado tamaño, su construcción no tiene un fuerte impacto en el consumo de memoria, por lo que es preferible la creación de una nueva instancia antes que la modificación de una ya existente, evitando así side-effects derivados de la modificación de los mismos.
Cuando hablamos de primitivas, podemos asignar el valor a una variable y compararlo con el de otra:
$a = 1;
$b = 1;$a == $b; //true
Podemos reasignar el valor cuantas veces queramos, desechando el anterior valor:
$a = 1;
$a = 2;
$a = 3;$a == 1; //false
$a == 2; //false
$a == 3; //true
Y también podemos modificarlos mediante operaciones, recibiendo siempre un nuevo valor:
$a = 1;
$a = $a + 9;$a == 10; //true
En cambio si pasamos una primitiva como argumento de una función, lo hacemos por valor (a menos que explícitamente lo hagamos por referencia) y la función no puede modificar su valor si no es asignando el resultado de la misma a la variable:
/** Forma Incorrecta */function add(int $a, int $b)
{
$a = $a + $b;
}$a = 1;
$b = 9;
add($a, $b);$a == 10; //false/** Forma correcta */function add(int $a, int $b)
{
return $a + $b;
}$a = 1;
$b = 9;
$a = add($a, $b);$a == 10; //true
La idea es que un VO se comporte igual que un tipo básico.
Siempre válido
Al constructor de un VO se le pasan siempre las primitivas (u otros VO) necesarias para instanciar el nuevo objeto, de manera este siempre estará en un estado válido, ya que en caso de faltar algún valor o proveer valores inválidos, este no se construirá y lanzará una excepción.
Una buena práctica consiste en proveer constructores semánticos estáticos que faciliten la creación de los VO, especialmente cuando uno de los parámetros tiene un conjunto limitado de valores válidos.
No poseen identidad
No hay que confundir nunca una entidad con un value object. La principal diferencia es que las primeras poseen una identidad, un identificador que las hace únicas de cara a otra instancia de la misma clase. Un value object en cambio no posee identidad, por lo que las comparaciones entre value objects deben hacerse basándose en su contenido, y no un identificador o referencia.
Esto choca bastante con el comportamiento por defecto de la mayoría de lenguajes de programación. Veamos el siguiente ejemplo:
<?php declare(strict_types=1);
class Point
{
private $x;
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX(): float
{
return $this->x;
}
public function getY(): float
{
return $this->y;
}
public function equalsTo(Point $point)
{
return $point === $this;
}
}
$a = new Point(2, 3);
$b = new Point(2, 3);
var_dump($a->equalsTo($b)); // Returns false
<?php declare(strict_types=1);
class Point
{
private $x;
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX(): float
{
return $this->x;
}
public function getY(): float
{
return $this->y;
}
public function equalsTo(Point $point)
{
return ($point->x === $this->x) && ($point->y === $this->y);
}
}
$a = new Point(2, 3);
$b = new Point(2, 3);
var_dump($a->equalsTo($b)); // Returns true
Como se puede observar en el primer ejemplo, hemos definido una clase Point
y hemos creado dos instancias iguales, pero al ejecutar el método equalsTo
el resultado no es el esperado, ya que la comprobación se está haciendo en base a la referencia del objeto, y no de su contenido.
En el segundo caso, la comparación sí devuelve el resultado esperado, porque hemos hecho la comparación en base al contenido del objeto.
Encapsulación
Supongamos que tenemos una clase Product
que entre otros contiene los atributos amount
y currency
. Dado este esquema, tendríamos que proveer por separado los dos parámetros, y en caso de requerir una validación de la currency para saber si es válida o es admitida por nuestro sistema ¿dónde la pondríamos? En la clase Product
no parece tener mucho sentido, y además rompe con el Principio de Única Responsabilidad. Además, parece que esa validación la vamos a reutilizar muchas veces y no solo en el contexto de un producto.
Si por contra creamos un VO Price
que nos permita encapsular la cantidad y la moneda y además ejecute todas la validaciones necesarias durante su construcción, estaremos simplificando las validaciones y la reutilización de las mismas, así como encapsulando de forma eficiente atributos que individualmente no tienen sentido por si mismos en el contexto de su uso.
Testing de Value Objects
Es bastante obvio que testear estos pequeños objetos es una tarea bastante sencilla, ya que prácticamente no tienen lógica más allá de las validaciones en sus constructores, pero no olvidemos que los Value Object deben de ser inmutables, y durante nuestro testeo tenemos que asegurarnos de que así es, comprobando que no hay side-effects al ejecutar cualquier operación con ellos.
Si por ejemplo nuestra clase Temperature
que representa la temperatura tuviese un método increase(Temperature $temperature)
, tendríamos que asegurarnos de que después de llamar al método, la instancia no ha sido modificada, sino que ha devuelto un nuevo VO Temperature con el nuevo valor incrementado.
Ejemplo
Dados los principios que hemos visto, a continuación podemos ver un ejemplo de un VO que representa una velocidad:
<?php declare(strict_types=1);
class Speed
{
const KILOMETERS_PER_HOUR = 'km/h';
const MILES_PER_HOUR = 'm/h';
private $amount;
private $magnitude;
private function __construct(int $amount, string $magnitude)
{
$this->checkAmount($amount);
$this->amount = $amount;
$this->magnitude = $magnitude;
}
private function checkAmount(int $amount)
{
if (0 > $amount) {
throw new NegativeSpeedException();
}
}
public static function inKilometersHour(int $amount): self
{
return new self($amount, self::KILOMETERS_PER_HOUR);
}
public static function inMilesPerHour(int $amount): self
{
return new self($amount, self::MILES_PER_HOUR);
}
public function getAmount(): int
{
return $this->amount;
}
public function getMagnitude(): string
{
return $this->magnitude;
}
public function increase(Speed $speed): self
{
if ($speed->getMagnitude() !== $this->magnitude) {
throw new MagnitudeMismatchException();
}
$newAmount = $this->amount + $speed->getAmount();
return new self($newAmount, $this->magnitude);
}
public function equalsTo(Speed $speed): bool
{
return
($this->amount === $speed->getAmount())
&&
($this->magnitude === $speed->getMagnitude())
;
}
public function __toString(): string
{
return $this->amount . $this->magnitude;
}
}
<?php declare(strict_types=1);
use PHPUnit\TestCase;
class SpeedTest extends TestCase
{
/**
* @test
* @expectedException NegativeSpeedException
*/
public function itShouldThrownAnExceptionWhenAmountIsNegative()
{
Speed::inKilometersPerHour(0);
Speed::inMilesPerHour(0);
}
/** @test */
public function itShouldCompareWithOthers()
{
$speedA = Speed::inKilometersPerHour(100);
$speedB = Speed::inKilometersPerHour(100);
$this->assertTrue($speedA->equalsTo($speedB));
$speedB = Speed::inMilesPerHour(100);
$this->assertFalse($speedA->equalsTo($speedB));
}
/** @test */
public function originalSpeedShouldNotBeModifiedOnIncrease()
{
$speedA = Speed::inKilometersPerHour(100);
$speedB = Speed::inKilometersPerHour(100);
$speedA->increase($speedB);
$this->assertEquals(100, $speedA->getAmount());
}
/** @test */
public function speedShouldBeIncreased()
{
$speedA = Speed::inKilometersPerHour(100);
$speedB = Speed::inKilometersPerHour(100);
$speedC = $speedA->increase($speedB);
$this->assertEquals(200, $speedC->getAmount());
}
/**
* @test
* @expectedException MagnitudeMismatchException
*/
public function speedShouldNotBeIncreasedIfMagnitudeIsDifferent()
{
$speedA = Speed::inKilometersPerHour(100);
$speedB = Speed::inMilesPerHour(100);
$speedC = $speedA->increase($speedB);
}
}
Como se puede ver:
- Mide, cuantifica o describe un concepto.
- Es inmutable.
- Provee constructores semánticos para su construcción.
- Se válida en la construcción, con lo que siempre tendrá un estado válido.
- Se compara por valor, no por referencia (No posee identidad).
- Encapsula en una clase dos atributos que por si solos no tienen sentido
Conclusión
Aunque puede parecer al principio un poco engorroso modelar cada concepto de nuestro sistema como un objeto, las ventajas a largo plazo son más que evidentes, tanto a nivel de implementación y reutilización de código como a nivel de testing.
Es cierto que el mapeado con la base de datos puede ser más complejo con entidades compuestas por VO, pero si utilizamos ORMs basados en Data Mapper (como Doctrine) la tarea se simplifica mucho más, esto lo veremos más adelante en otro post.
Referencias
- Domain-Driven Design in PHP, por Carlos Buenosvinos, Christian Soronellas y Keyvan Akbary
- Domain-Driven Design: Tackling Complexity in the Heart of Software por Eric J. Evans
- Implementing Domain-Driven Design por Vaughn Vernon
- Domain-Driven Design (Distilled) Versión reducida del anterior libro.