
No me gustan los setters (y lombok lo empeora)
Decir que a uno no le gustan los setters quizás sea una afirmación un tanto vehemente, y quizás sólo haya titulado así la entrada como cebo. La verdad es que sólo es una excusa para hablar de algunas cosas tangencialmente relacionadas, como por qué los setters entorpecen el diseño de nuestras entidades. De hecho iba a centrarme más el lombok, pero he cambiado de idea mientras escribía. Pero vamos primero con un párrafo introductorio.
¿Qué es lombok?
Lombok es una librería de Java que permite generar código automáticamente en nuestras clases a través de ciertas anotaciones. Además se integra muy bien con la mayoría de IDEs y agiliza el proceso de desarrollo al evitarnos tener que escribir código repetitivo.
Por ejemplo, si anotamos con @Getter nuestra clase, genera getters para todas sus propiedades.
Esto:
@Getter
class Clase {
private int numero;
private String texto;
}
Se convertiría en esto:
class Clase {
private int numero;
private String texto;
public int getNumero() {
return this.numero;
}
public String getTexto() {
return this.texto;
}
}
Una pasada, ¿a que sí? Al menos eso es lo que me pareció cuando lo conocí, seguramente en la carrera hace casi 15 años. Además no sólo hace getters. También setters. Y constructores, y métodos toString, equals y mil cosas más.
Pero…
Bueno, supongo que hace 15 años yo era más impresionable y los IDEs eran peores. Mucho peores. A día de hoy los IDEs modernos ya tienen formas de generar esta clase de código. Me cuesta menos generar getters y setters con un atajo de teclado que escribir @Getter. Aún así no es ese el motivo de mi desenamoramiento. Sigue siendo más limpia la anotación.
Los setters, el verdadero problema
A veces, y me refiero en concreto a los setters, se abusa de lombok. En realidad esto no es exclusivo de lombok ni de Java. Añadimos setters por defecto en lugares en que no tienen sentido.
Si en una clase todas sus propiedades pueden ser leídas y asignadas, ¿por qué utilizar modificador de acceso private para estas propiedades? De hecho es la mejor forma de romper la encapsulación. Esto es especialmente sangrante en las entidades del dominio, entendidas desde el punto de vista del DDD. Esto nos suele llevar a un modelo anémico, un modelo que no es responsable de su propia lógica. Pero vamos a ilustrarlo con algunos ejemplos facilones.
Imaginemos que tenemos una aplicación para gestionar recetas de cocina. Uno de nuestros campos es la duración aproximada de la receta, en minutos. Para ello tenemos un campo duración en la entidad Receta que representa este valor. Tendría sentido suponer que una receta tiene que costar más de 0 minutos. Con lombok/setters por defecto tendríamos esto:
public class Receta {
private Long duracion;
// getters y setters del resto de campos
public Long getDuracion() {
return this.duracion;
}
public void setDuracion(Long duracion) {
this.duracion = duracion;
}
}
Ahora, si quisiésemos validar que la duración es mayor de 0 minutos, en nuestro servicio que crea las recetas tendríamos algo como esto:
private static final Long MIN_DURACION_RECETA = 0;
public void crearReceta(Long duracion /* aquí irían el resto de campos, por ejemplo */) {
Receta receta = new Receta();
// Aquí asignaríamos el resto de campos
if(duracion <= MIN_DURACION_RECETA) {
throw new Exception("La duración debe ser mayor que 0");
}
receta.setDuracion(duracion);
this.recetaRepository.save(receta);
}
¿Tiene mucho sentido esto? ¿Y si creásemos la receta desde otro servicio, tendríamos que incluir ahí también la validación? Hay otras formas de asegurar la validación, en mi opinión igual de malas. Una vez vi una entidad en la que la validación se hacía en un método @PrePersist, de forma que no sabías qué estaba mal hasta que ibas a guardar la entidad. También podríamos poner la validación en una constraint de la base de datos. O que se encargue el frontend y confiar ciegamente en lo que nos envía. Y seguro que hay un millón de formas más de repartir la lógica de negocio por diferentes sitios menos en el que debe estar, en el modelo.
| Dónde se valida | Pros | Contra | |
|---|---|---|---|
| Validación en el servicio | En la capa de servicio antes de asignar los valores a la entidad. | Separa validación de la entidad; fácil de leer en servicios pequeños. | La lógica se dispersa, riesgo de inconsistencias si hay múltiples servicios que crean/modifican la entidad. |
| Validación en un método @PrePersist | Antes de persistir la entidad en la base de datos. | Centraliza validación en la entidad, evita datos inválidos en la BD. | No detecta errores hasta que se intenta guardar la entidad, lo que puede causar fallos tardíos y difíciles de depurar. |
| Validación con constraints en la base de datos | A nivel de esquema SQL (ej. CHECK (duracion > 0)). | Garantiza integridad a nivel de BD, no permite estados inválidos. | La validación ocurre tarde, errores difíciles de manejar en código. No cubre reglas complejas. |
| Validación en la entidad | Dentro de la entidad, en el constructor o en métodos de negocio. | Centraliza la lógica de negocio, impide la creación de objetos en estados inválidos. | Puede requerir patrones adicionales como fábricas o builders si hay muchas reglas complejas. |
Pero veamos ahora un ejemplo un poco más apropiado:
public class Receta {
private static final Long MIN_DURACION_RECETA = 0;
private Long duracion;
// getters y setters del resto de campos
public Long getDuracion() {
return this.duracion;
}
public void setDuracion(Long duracion) {
if(duracion <= MIN_DURACION_RECETA) {
throw new Exception("La duración debe ser mayor que 0");
}
this.duracion = duracion;
}
}
Y el servicio quedaría así:
public void crearReceta(Long duracion /* aquí irían el resto de campos, por ejemplo */) {
Receta receta = new Receta();
// Aquí asignaríamos el resto de campos
receta.setDuracion(duracion);
this.recetaRepository.save(receta);
}
Un poco mejor, ahora el servicio se encarga únicamente de coordinar los distintos elementos de la capa de dominio, pero no tiene ningún conocimiento de la lógica de negocio. Aun así sigue sin estar del todo bien. ¿Qué pasa si hago lo siguiente?
public void crearReceta() {
Receta receta = new Receta();
this.recetaRepository.save(receta);
}
Tal y como hemos modelado la receta, esto es válido, pero el modelo no representa correctamente nuestro dominio. ¿Puede una receta no tener duración? ¿O nombre? Estamos permitiendo que nuestra receta esté en un estado inconsistente. Y forzamos a mantener esa consistencia en cualquier servicio que instancie una receta. Así que vamos a modificar la entidad un poco.
public class Receta {
private static final Long MIN_DURACION_RECETA = 0;
private String nombre;
private Long duracion;
protected Receta() {} // Aquí impedimos que se // utilice el constructor por defecto
public static Receta create(String nombre, Long duracion) {
Receta receta = new Receta();
receta.setNombre(nombre);
receta.setDuracion(duracion);
return receta;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public void setDuracion(Long duracion) {
if(duracion <= MIN_DURACION_RECETA) {
throw new Exception("La duración debe ser mayor que 0");
}
this.duracion = duracion;
}
}
Hemos cambiado la visibilidad del constructor por defecto, para que no pueda utilizarse, y creado un named constructor o static factory method (no confundir con el factory method de verdad, que es para otra cosa) o como lo queráis llamar. También podríamos haber creado un constructor con esos argumentos, pero uno a trabajado mucho tiempo con PHP (donde sólo puede haber un constructor) y además creo que queda más claro. Y quizás os estéis preguntando “¿Y por qué protected en lugar de private?”. Porque si da la casualidad de que estáis trabajando con Spring e Hibernate, las entidades tienen que tener el constructor por defecto para que Hibernate haga su magia de materialización por debajo, pero no funciona con visibilidad private.1
El servicio ahora quedaría así:
public void crearReceta(String nombre, Long duracion) {
Receta receta = Receta.create(nombre, duracion);
this.recetaRepository.save(receta);
}
Incluso podríamos hacer private los setters.
Vale, pero ¿y si quiero modificar, no crear?
Incluso a la hora de modificar una entidad, siguen sin gustarme los setters, al menos públicos. Los cambios importantes en una entidad suelen implicar varios campos. Permitir que se modifiquen por separado muchas veces no tiene sentido. Además no tiene sentido el verbo asignar para muchos de los cambios que se realizan sobre el estado de nuestra entidad. ¿Vosotros asignáis un nuevo conjunto de coordenadas espaciales de una silla o movéis una silla? Pero vamos ahora con el ejemplo malo.
Imaginad que en nuestra aplicación de recetas, antes de ser mostradas al resto de usuarios, deben estar aprobadas por un prestigioso chef. De hecho, como imaginar es gratis, va a ser Karlos Arguiñano. Cuando revise una receta podrá darle a un botoncito de aprobar o de rechazar. Vamos a modificar un poco la entidad para reflejar estos cambios:
public class Receta {
// ...
private Chef revisor;
private Estado estado;
// ...
public void setRevisor(Chef revisor) {
this.revisor = revisor;
}
public void setEstado(Estado estado) {
this.estado = estado;
}
}
Podemos suponer que estado es un enum con todos los posibles estado de una receta como aprobada, rechazada, pendiente de revisión, etc. Ahora tendríamos esto en el servicio:
public void revisarReceta(Receta receta, Chef revisor, Estado estado) {
receta.setRevisor(revisor);
receta.setEstado(estado);
this.recetaRepository.save(receta);
}
¿Podría cambiar el estado sin que cambiase el revisor? ¿Y al revés? Tendría más sentido que se realizase en una única operación, ¿verdad?
public class Receta {
// ...
private Chef revisor;
private Estado estado;
// ...
public void revisar(Chef revisor, Estado estado) {
this.revisor = revisor;
this.estado = estado;
}
}
De hecho, el algún caso incluso podríamos tener métodos distintos para aprobar y rechazar la receta, que reflejasen exactamente la naturaleza del cambio. En el caso anterior quizás no tendría mucho sentido, pero supongamos este nuevo caso: a la hora de aprobar las recetas confiamos plenamente en el criterio de nuestros chefs, pero Karlos quiere ser constructivo a la hora de rechazar una receta y quiere poder añadir los motivos y posibles mejoras, así que añadimos un campo motivoRechazo.
Ahora, para aprobar la receta sólo se necesita el chef, pero para rechazarla se necesita el chef y el motivo:
public class Receta {
// ...
private Chef revisor;
private Estado estado;
private String motivoRechazo;
// ...
public void aprobar(Chef revisor) {
this.revisor = revisor;
this.estado = Estado.APROBADA;
}
public void rechazar(Chef revisor, String motivoRechazo) {
this.revisor = revisor;
this.estado = Estado.RECHAZADA;
this.motivoRechazo = motivoRechazo;
}
}
En resumen…
Como habréis observado, hemos hablado muy poco de lombok y algo más de los setters. Es indudable que toda herramienta que permita ahorrar tiempo de escribir código es útil, pero hay que utilizarla con criterio. Y con mayor motivo en la parte más importante de nuestra aplicación. Nuestro código debería expresar qué hace, no cómo lo hace. Y nuestras entidades deberían representar fielmente nuestro modelo, haciéndose responsables de su propia lógica.
- Hay que usar Lombok con criterio, no para generar código indiscriminadamente.
- Las entidades deben modelarse en función de su comportamiento, no únicamente por sus datos.
- Los métodos deberían ser expresivos,evitando setters genéricos.
Foto de Łukasz Rawa en Unsplash
A 29 de noviembre de 2025: Qué tonto era, por Dios. Esta entrada sería bastante distinta ahora. Anda que usar entidades JPA como entidades de dominio… ↩︎