7 Records¶
Patrón de diseño DTO¶
Una de las problemáticas más comunes cuando desarrollamos aplicaciones, es diseñar la forma en que la información debe viajar desde una capa de la aplicación a otra capa, ya que muchas veces por desconocimiento o pereza, utilizamos las clases de entidades para retornar los datos, lo que ocasiona que retornemos más datos de los necesarios, o incluso, tengamos que ir en más de una ocasión a la capa de servicios para recuperar los datos requeridos.
El patrón de diseño DTO (Data Transfer Object) tiene como finalidad la creación de objetos planos (POJO) con una serie de atributos que puedan ser enviados o recuperados del servidor en una sola invocación, de tal forma que un DTO puede contener información de múltiples fuentes o tablas y concentrarlas en una única clase simple.
Supongamos que tenemos dos tablas en la base de datos, una de clientes (Customers) y otra con su dirección (Address). En esta última, se hace referencia a la tabla clientes, pero en clientes no existe una referencia a la tabla dirección, por lo que nos encontrados en una relación 1:N.
El problema con esta relación viene cuando queremos crear un servicio que retorne al cliente con su dirección, pues la entidad Customer
no tendrá una relación al objeto Adress
, lo que nos obligará a hacer dos llamadas al backend para recuperar al cliente y la dirección por separado o terminar contaminando la entidad al agregar el campo address que no es necesario para la persistencia.
Como vemos, utilizar una Entity o cualquier otro objeto que allá sido creado para otro propósito diferente que el de ser usado para transmisión de datos puede tener complicaciones, por lo que el patrón DTO propone que en lugar de usar estas clases, creemos clases especiales para transmitir los datos, de esta forma, podemos controlar los datos que enviamos, el nombre, el tipo de datos, etc, además, si estos necesitan cambiar, no tiene impacto sobre la capa de servicios o datos, pues solo se utilizan para transmitir la respuesta. Dicho lo anterior, retornemos al ejemplo anterior, pero utilizando un DTO:
Para que una clase sea considera DTO debe cumplir algunas reglas:
- Debe ser una clase solo de lectura, es decir, no deben de crearse los métodos setters ni otros métodos que modifiquen directamente el valor de un atributo. Además, dichos atributos deben ser constantes por lo que deberían usar el modificador de acceso final.
- También, deben ser serializables, tanto la clase, como cada uno de sus objetos.
Veamos un ejemplo:
public class CustomerDTO implements Serializable{
private final int id;
private final String fullName;
private final String country;
private final Address address; // !(1)
private final int cp;
private final String date;
public CustomerDTO(int id, String fullName, String country, Address address, int cp, String date){
this.id = id;
this.fullName = fullName;
this.country = country;
this.address = address;
this.cp = cp;
this.date = date;
}
public int getId(){
return id;
}
public String getFullName(){
return fullName;
}
public String getCountry(){
return country;
}
public Address getAddress(){
return address;
}
public int getCp(){
return cp;
}
public String getDate(){
return date;
}
@Override
public boolean equals(Object o){
/* ... */
}
@Override
public boolean hashCode(){
/* ... */
}
@Override
public String toString(){
/* ... */
}
}
- La clase
Address
debe ser también serializable, así como, todos los atributos de la misma.
Ejercicio 1
Haciendo uso del patrón de diseño DTO, crea las clases necesarias para almacenar los autores y libros de una biblioteca.
De loas autores se desea almacenar:
- Código identificativo.
- Nombre del autor
- Año de nacimiento
- Año de defunción (si procede)
De los libros se desea almacenar:
- Código ISBN en base 13
- Código ISBN en base 10
- Nombre del libro
- El autor que ha escrito el libro
- Año de publicación
- Nombre de la editorial
Records¶
Los registros o records son una característica nueva de Java incorporada inicialmente como previa en la versión 14, y posteriormente movida a estable en el 17. Se trata de una implementación del patrón DTO, de otros lenguajes de programación. Son clases que se usan para almacenar valores y poder agruparlos en un único identificador, de manera fundamental.
Por lo general únicamente se especifica qué atributos se interesa que tenga un registro, y al compilar un registro, el compilador se ocupará de generar constructores, getters y métodos, así como toString()
, equals()
o hashCode()
de forma automática. En definitiva, parte del atractivo de los registros es permitir conseguir algo parecido a lo que podemos hacer con clases tradicionales de Java, pero utilizando mucho menos código.
La estructura de un record es la siguiente:
Como se puede observar es una estructura muy parecida a las clases, pero con dos diferencias cruciales:
- Se utiliza la palabra reservada
record
en lugar de la palabra reservadaclass
. De esta manera, el compilador será consciente de cuando una clase es una clase normal o es una clase que implementa el patrón DTO. - Por otro lado, la declaración de los atributos de la clase se realizan en la firma de la misma, utilizando los paréntesis
()
como si fuese una especie de constructor. Estos atributos se crean directamente con el modificador de accesoprivate final
.
public record Customer(int id, String fullName, String country, Address address, int cp, String date){
}
Por defecto, la clase record se traduciría a algo, tal que así:
public class Customer implements Serializable{
private final int id;
private final String fullName;
private final String country;
private final Address address;
private final int cp;
private final String date;
public CustomerDTO(int id, String fullName, String country, Address address, int cp, String date){
this.id = id;
this.fullName = fullName;
this.country = country;
this.address = address;
this.cp = cp;
this.date = date;
}
public int id(){
return id;
}
public String fullName(){
return fullName;
}
public String country(){
return country;
}
public Address address(){
return address;
}
public int cp(){
return cp;
}
public String date(){
return date;
}
@Override
public boolean equals(Object o){
if(this == 0) return true;
if(o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id == o.id && fullName.equals(o.fullName)
&& country.equals(o.country) && address.equals(o.address)
&& cp.equals(o.cp) && date.equals(o.date);
}
public boolean hashCode(){
return Objects.hash(id, fullName, country, address, cp, date);
}
public String toString(){
return "Customer[id="+id+", fullName="+fullName+", country="+country+" address="+address+", cp="+cp+", date="+date+"]";
}
}
Observe que los métodos getters no son convencionales, es decir, no empiezan con el prefijo get seguido del nombre del atributo, si no que directamente es el nombre del atributo.
Ejercicio 2
Realiza el ejercicio anterior, haciendo uso de records.
Métodos adicionales¶
Además de los métodos que el record te crea por defecto, también se pueden crear métodos adicionales:
Una limitación importante a tener en cuenta, es que los métodos creados no deben modificar los valores de los atributos, ya que éstos por defectos son final
, es decir, constantes. Por consecuencia, no se pueden crear métodos setters.
public record Customer(/*...*/){
public String username(){
return '@' + fullName;
}
public void setFullName(String fullName) { // ❌ ERROR COMPILACIÓN ❌
this.fullName = fullName;
}
}
Ejercicio 3
A las clases anteriores, creales los métodos siguientes que sean posible:
- Método para modificar el nombre del libro
- Método que devuelva el valor de ISBN13 o 10 con el siguiente formato:
ISBN13:XXXX-XXXX-XXXX-XXXX-X
- Método que añada 10 años a la fecha de la publicación, para en caso de que haya habido un error al crearlo.
- Método que muestre la información del libro en forma de tabla.
Constructores auxiliares¶
También es posible crear constructores adicionales para un record. No obstante, existe la obligación de llamar en algún momento al constructor completo, porque todos los atributos del record deben recibir un valor inicial. Por lo que no se podría crear un constructor vacío:
public record Customer(/* ... */){
public Customer(){ // ❌ ERROR COMPILACIÓN ❌
System.out.println('Creating empty record...');
}
}
Tampoco se debe crear un constructor, el cual se de valores a unos atributos y otros no. En ese caso, será necesario indicar un valor por defecto:
public record Customer(/* ... */){
public Customer(int id, String fullName, String country, Address address, int cp){
this(id, fullName, country, address, cp, '2024-06-06');
}
}
Como vemos en el ejemplo anterior, se ha añadido un constructor dónde no se pasa la fecha, pero tiene por defecto la fecha actual.
Ejercicio 4
Añade un constructor a la clase libro, que reciba todos los parámetros, excepto el año de publicación, para aquellos libros que aún no han sido publicados. Cuando un libro no ha sido publicado se almacenará en una base de datos el valor -1.