Patrón Value-Object

Lambdaloopers
5 min readAug 31, 2021

Una de las claves para desarrollar código mantenible en programación orientada a objetos es la correcta encapsulación de las lógicas en las clases que modelan nuestros conceptos de dominio.

Por ejemplo, consideremos el siguiente caso de uso, en el que queremos registrar un usuario en nuestra aplicación:

public final class UserRegisterer {    // Dependencies and constructor    public void register(String email) {
if (!isValid(email)) {
throw new InvalidEmail();
}
User user = new User(email);
repository.save(user);
}
private boolean isValid(String email) {
// Some email validation implementation
}
}

Aquí estamos haciendo la validación del email a nivel de caso de uso. Eso quiere decir que en cada caso de uso donde tengamos que tener en cuenta esta lógica deberemos de replicarla, con el peligro de que en algún caso se nos olvide. O incluso de que, cuando en el futuro se modifique esta lógica, hayamos de cambiarla en todos los puntos donde se aplica.

Un primer paso que podemos dar para mejorar esto es encapsular la validación dentro de la clase User para que en todos los casos de uso (como registrar usuario, editar usuario...) esta validación se haga automáticamente de manera interna.

public final class User {
private int id;
private String email;
public User(email) {
setEmail(email);
}
public void setEmail(String email) {
if (!isValid(email)) {
throw new InvalidEmail();
}
this.email = email;
}
private boolean isValid(String email) {
// Some email validation implementation
}
}

Parece que este debería ser el patrón a aplicar. Sin embargo, ahora desde producto nos piden que los usuarios pueden tener una agenda de contactos con un nuevo caso de uso:

public final class ContactCreator {    // Dependencies and constructor    public void addContact(String email, int userId) {
User user = userRepository.findById(userId);
// We need to validate the email!

Contact contact = new Contact(email, user)
contactRepository.save(contact);
}
}

¿Qué podemos hacer ahora? Como desarrollador se me ocurren muchos parches que podría generar esto, desde duplicar el método isValid hasta hacerlo público en la clase User y encontrarnos aberraciones del estilo

user.isValid(email) // Don't do this!!

(y no disimuléis, que no os sorprende tanto).

Está claro que nos hemos encontrado con un escollo que no nos permite avanzar tal y como quisiéramos. Algo que parece que nos obliga a escribir código feo y sin sentido. Está claro que esto nos indica que algo hemos hecho mal. Yo tengo la siguiente máxima:

Si tu modelo describe adecuadamente el problema que quieres resolver, el código encajará y fluirá a la perfección. Por otro lado, si te encuentras con que tienes que hacer trampas en el código para desarrollar tu aplicación, esto es un indicador de que tus abstracciones deben ser revisadas.

En este caso, el problema se origina porque estamos tratando de atribuir propiedades al concepto email sin haber creado una abstracción que lo represente. Email es un concepto que no depende de Usuario ni tampoco de Contacto, sino que tiene sentido por sí mismo. Tiene datos (su propio valor) y comportamiento (sabe si es válido o no), por lo tanto se merece una clase.

public final class EmailAddress {
private final String value;
public EmailAddress(String value) {
if (!isValid(email)) {
throw new InvalidEmail();
}
this.value = value;
}
private boolean isValid(String value) {
// Some email validation implementation
}
}

Notemos que hemos hecho el campo value inmutable con la keyword final. Esto es debido a una característica definitoria del patrón que estamos usando. Se trata del patrón Value-Object. Un value-object es una clase que representa un modelo, como lo hacen las entidades, pero a diferencia de estas, los value-objects no tienen una identidad propia que les distingue del resto, sino que vienen definidos exclusivamente por, precisamente, su valor.

Por ejemplo, una persona viene definida por una identidad propia. Si esa persona se cambia de ropa, se muda, se tiñe el pelo, se cambia de nombre o incluso de sexo, seguirá siendo la misma persona, pero con una serie de propiedades diferentes a antes de ese cambio. Normalmente al modelar una persona (o cualquier otra entidad) en una aplicación representamos esta identidad única con un identificador único (un número o una cadena de caracteres).

Sin embargo, una dirección de email, una dirección de correo, un teléfono… son conceptos que vienen definidos por los propios datos que representan. Si estos datos cambiaran, los mismos conceptos serían diferentes. Si comparamos la dirección de correo user@mail.com y hello@company.com no diremos que son la misma donde se han cambiado algunos datos entre una y otra. Incluso propiedades con múltiples valores, como por ejemplo

public final class Address {
private final String streetName;
private final int streetNumber;
// Constructor, etc.
}

No diremos que la dirección “Calle Mayor, 1” es la misma que “Plaza Central, 1” o que “Calle Mayor, 5”. Todo esto deriva en que no tiene sentido modificar los campos de un value-object, porque entonces pasa a ser otro value-object diferente, por lo tanto

Los value-objects han de ser modelos inmutables.

Otras ventajas que nos aportan los value-objects son:

Por ejemplo, consideremos la siguiente clase de Contacto:

public final class Contact {
private int id;
private String firstName;
private String lastName;
private String email;
private String phoneNumber;
private String streetName;
private int streetNumber;
private String city;
private String zipCode;
private String country;
public Contact(
String firstName,
String lastName,
String email,
String phoneNumber,
String streetName,
int streetNumber,
String city,
String zipCode,
String country) {
// ...
}
}

Cada vez que tengamos que instanciar un contacto usando el constructor tendremos que tener mucho cuidado de poner la string correcta en la posición adecuada. Comparémoslo con:

public final class Contact {
private ContactId id;
private Name name;
private EmailAddress email;
private PhoneNumber phoneNumber;
private Address address;
public Contact(
ContactId id,
Name name,
EmailAddress email,
PhoneNumber phoneNumber,
Address address) {
this.id = id;
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber;
this.address = address;
}
}

Como conclusión, no te limites a modelar tu dominio usando simplemente entidades. Llenar tu codebase de estas sencillas clases te proporcionará unas ventajas increíbles a la hora de aplicar adecuadamente la lógica de tu negocio.

David Pravos — Tech Lead @LambdaLoopers

--

--

Lambdaloopers

We are the team that thinks, works and aims to ensure you a successful digital journey. 👨🏻‍💻👩🏻‍💻 #webdevelopment #digital #technology