¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
José Luis Canales 23/10/2024 Cargando comentarios…
Tengo la costumbre de leer y repasar artículos técnicos o blogs sobre software en general que hayan sido publicados hace tiempo. Por “tiempo” me refiero a lustros o décadas. Suelo encontrar estos documentos ricos en principios y pobres en anotaciones java, lo cual me resulta más estimulante.
En uno de ellos, relativamente reciente en mi escala temporal, he releído lo siguiente:
“Continuous attention to technical excellence and good design enhances agility. “
Este es uno de esos principios derivados del Agile Manifesto a los que se da poca publicidad y se olvidan rápido. En mi caso, suelo refrescarlo cada cierto tiempo porque despierta la curiosidad sobre el sentido de algunas prácticas que veo e incluso utilizo, con el objetivo de conocer sus fundamentos y revisar si se pueden hacer las cosas mejor.
En este caso, me he decantado por divagar acerca del uso de getters y setters de forma indiscriminada en nuestros desarrollos.
Lo cierto es que la inclusión de getters y setters en las clases java es algo tan extendido que ya ni siquiera tienes que hacerlo. El propio IDE te los genera sin que tengas que emplear esfuerzo.
Por resumirlo de forma común, no es más que exponer atributos privados de una clase a través de funciones para obtenerlos y modificarlos.
De forma más concreta, es algo como esto:
public class Person
{
private String firstName;
private String lastName;
private Set<String> emails;
public String getFirstName(){ return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName;}
public String getLastName(){ return lastName; }
public void setLastName(String lastName){ this.lastName = lastName; }
public Set<String> getEmails(){ return emails; }
public void setEmails(Set<String> email){ this.emails = email; }
}
Otros desarrolladores prefieren eliminar código ‘BoilerPlate’ por lo que, añadiendo lombok a su proyecto, consiguen reescribirlo así:
@Getter
@Setter
public class Person
{
private String firstName;
private String lastName;
private Set<String> emails;
}
Si nos fijamos en las implicaciones que esta práctica tiene para la clase en cuestión, podemos ver dos muy claras:
En efecto, todo esto puede sonar muy ‘clásico’. No en vano responde a los principios de OOP que tan de moda estuvieron en los años 90 del siglo pasado, pero no por eso carece de sentido. El encapsulamiento limita el impacto de los cambios sobre una clase a su ámbito interno y esta restricción reduce las probabilidades de fallo, a la par que facilita la localización del mismo si se produce.
Por otro lado, a pesar de los inconvenientes anteriores, siempre se puede argumentar que, en la construcción de determinados objetos o en algunos métodos, puede ser más conciso y legible utilizar una estructura de datos organizada que innumerables argumentos. Qué mejor que utilizar uno de los famosos POJO con sus getters y sus setters.
Considerando que durante el diseño de la aplicación pueden concurrir las necesidades expuestas anteriormente (simples estructuras de datos o entidades que encapsulan su estado y la lógica de gestión), me lleva a preguntarme si existen alternativas más simples o robustas de conseguir ese comportamiento.
En el caso de necesitar una simple estructura de datos que agrupe y ordene un conjunto de información, yo me inclino por la siguiente alternativa:
public class Person
{
public String firstName;
public String lastName;
public Set<String> emails;
}
Puede chocar el declarar todos los atributos como públicos pero, al fin y al cabo, esta estructura expone los atributos igual que si utilizáramos getters y setters, obteniendo al mismo tiempo la ventaja de no añadir código ni librerías adicionales al proyecto.
Por otro lado, si programamos objetos, lo más adecuado es seguir sus principios, entre los que no se recomienda dar acceso libre a los atributos que almacenan el estado de dicho objeto. Además de esto, se debería gestionar los cambios de estado de forma que se respeten las reglas que mantienen la coherencia del mismo.
Una posible alternativa podría ser:
public class Person
{
private final String _firstName;
private final String _lastName;
private final List<String> _emails;
public Person(String firstName,
String lastName)
{
this._firstName = Objects.requireNonNull(firstName);
this._lastName = Objects.requireNonNull(lastName);
_emails = new ArrayList<>();
}
public String firstName(){ return _firstName;}
public String lastName(){ return _lastName;}
public List<String> emails(){ return new ArrayList<>(_emails);}
public void addEmail(String email)
{
if (email != null && !_emails.contains(email))
_emails.add(email);
}
public void removeEmail(String email)
{
if(email != null)
_emails.remove(email);
}
}
Por supuesto, también podemos optar por hacer una versión inmutable de la misma entidad.
public class Person
{
private final String _firstName;
private final String _lastName;
private final List<String> _emails;
public static Person of(String firstName,
String lastName)
{
return new Person(firstName,
lastName,
new ArrayList<>());
}
private Person(String firstName,
String lastName,
List<String> _emails)
{
this._firstName = Objects.requireNonNull(firstName);
this._lastName = Objects.requireNonNull(lastName);
this._emails = _emails;
}
public String firstName() {return _firstName;}
public String lastName() {return _lastName;}
public List<String> emails() {return new ArrayList<>(_emails);}
public Person addEmail(String email)
{
if (email != null && !_emails.contains(email))
{
List<String> emails = emails();
emails.add(email);
return new Person(_firstName,_lastName,emails);
}
return this;
}
public Person removeEmail(String email)
{
if (email != null && _emails.contains(email))
{
List<String> emails = emails();
emails.remove(email);
return new Person(_firstName,_lastName,emails);
}
return this;
}
}
No pretendo realizar una disquisición académica sobre esta práctica y la razón existencial de los POJO’s con sus getters y sus setters. Es una práctica que procuro evitar todo lo posible cuando diseño el dominio y la lógica de una aplicación.
No obstante, me pregunto cuándo fue el momento en que empecé a verla con el objetivo de conocer los puntos desde donde empieza a extenderse a todo el desarrollo.
En mi caso, empezó al implementar aspectos con las primeras versiones de Spring. Hasta entonces siempre había inyectado dependencias a través del constructor de mis objetos de lógica de negocio para asegurar su consistencia. El caso es que, para aplicar aspectos, Spring genera una clase proxy de la clase objetivo y, debido a su funcionamiento interno, no permite en la clase objetivo la inyección de dependencias a través del constructor. ¿Solución? Crear setters para todo aquello que debería garantizarse en el constructor (haciendo objetos inconsistentes que no funcionan tras su creación y que más de una vez me han traído un disgusto).
Esto se aplica a los frameworks que utilizamos para acceder a base de datos que precisan POJOs para el intercambio de datos, a las librerías para el mapeo de JSON y, por ende, la integración con servicios y un largo etcétera.
Por otro lado, si nos fijamos en alguno de los problemas que queremos solucionar computacionalmente, como por ejemplo un cálculo de nómina, aparecen conceptos tales como el empleado, el puesto, el salario base, los complementos, las retenciones, el salario en especie… Todos estos conceptos y las reglas que los combinan para el cálculo son absolutamente independientes de cosas como la persistencia de datos utilizada, el protocolo de acceso a servicios externos o los frameworks que dan soporte a la aplicación. La razón de esto es simple, los artefactos técnicos que utilizamos son detalles, no son parte fundamental del problema a resolver.
De alguna manera, hemos naturalizado y permitido que esta práctica, que puede ser imprescindible para interactuar con algunos de los frameworks que utilizamos, acabe contaminando el diseño completo de las aplicaciones permitiendo la creación de objetos inconsistentes y esparciendo la lógica que debería estar encapsulada en dichos objetos por toda la aplicación. Una vez más, lo fundamental sigue teniendo una fuerte dependencia del detalle y se encuentra ligado al mismo.
El problema de la ruptura de la dependencia entre los módulos de alto nivel y los detalles de bajo nivel es algo que, de hecho, está resuelto. Principios como la inversión de control tienen como objetivo propiciar esa ruptura mediante el uso de abstracciones del comportamiento que deben tener los módulos de bajo nivel.
En java, una forma de modelizar dichas abstracciones es mediante interfaces. No obstante, aunque una forma de implementar la inversión de control sea mediante interfaces (y los mecanismos posteriores de inyección de dependencias), el uso de interfaces no implica que se esté utilizando este principio. Cuando las interfaces no se diseñan desde la óptica de las necesidades del dominio del problema a resolver, sino que se diseñan desde la óptica de las necesidades del framework de turno, no hemos avanzado nada. La proliferación de POJOs con sus getters y sus setters por la aplicación es un indicador de que este problema se está dando.
El uso indiscriminado de getters y setters se debería evitar en nuestros diseños de aplicaciones ya que expone la estructura interna de los objetos. Esta exposición hace que el impacto de los cambios que afectan a los objetos se extienda por la aplicación en lugar de permanecer localizados y confinados al ámbito de la clase.
Existen diversas formas de implementar los objetos que hacen que esta práctica sea innecesaria. No obstante, tampoco podemos obviar que muchos de los apoyos técnicos que tenemos fuerzan a trabajar con objetos de este tipo.
Sin embargo, tenemos principios de diseño, como la inversión de control que, utilizados adecuadamente, deberían salvaguardar el correcto diseño de la lógica computacional de nuestras aplicaciones (lo fundamental) de los distintos frameworks que utilizamos como apoyo (lo accesorio) y los procedimientos que debamos utilizar para integrarnos con ellos. La correcta aplicación de estos principios permite excluir el uso de getters y setters del diseño de nuestras aplicaciones.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.