Los accesorios y modificadores (también conocidos como setters y getters) son útiles por tres razones principales:
- Restringen el acceso a las variables.
- Por ejemplo, se puede acceder a una variable, pero no modificarla.
- Validan los parámetros.
- Pueden causar algunos efectos secundarios.
Las universidades, los cursos en línea, los tutoriales, los artículos de blog y los ejemplos de códigos en la web hacen hincapié en la importancia de los accesores y modificadores, casi se sienten como un "must have" para el código hoy en día. Entonces uno puede encontrarlos incluso cuando no proporcionan ningún valor adicional, como el código a continuación.
public class Cat {
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
Dicho esto, es muy común encontrar modificadores más útiles, aquellos que realmente validan los parámetros y arrojan una excepción o devuelven un valor booleano si se ha proporcionado una entrada no válida, algo como esto:
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
Pero incluso entonces, casi nunca veo que los modificadores se hayan llamado desde un constructor, por lo que el ejemplo más común de una clase simple que enfrento es este:
public class Cat {
private int age;
public Cat(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
}
Pero uno pensaría que este segundo enfoque es mucho más seguro:
public class Cat {
private int age;
public Cat(int age) {
//Use the modifier instead of assigning the value directly.
setAge(age);
}
public int getAge() {
return this.age;
}
/**
* Sets the age for the current cat
* @param age an integer with the valid values between 0 and 25
* @return true if value has been assigned and false if the parameter is invalid
*/
public boolean setAge(int age) {
//Validate your parameters, valid age for a cat is between 0 and 25 years
if(age > 0 && age < 25) {
this.age = age;
return true;
}
return false;
}
}
¿Ves un patrón similar en tu experiencia o solo soy yo desafortunado? Y si lo haces, ¿qué crees que está causando eso? ¿Existe una desventaja obvia para usar modificadores de los constructores o simplemente se consideran más seguros? ¿Es algo más?
fuente
Respuestas:
Razonamiento filosófico muy general
Por lo general, pedimos que un constructor proporcione (como condiciones posteriores) algunas garantías sobre el estado del objeto construido.
Por lo general, también esperamos que los métodos de instancia puedan asumir (como condiciones previas) que estas garantías ya se mantienen cuando se llaman, y solo tienen que asegurarse de no romperlas.
Llamar a un método de instancia desde el interior del constructor significa que algunas o todas esas garantías aún no se han establecido, lo que dificulta razonar si las condiciones previas del método de instancia se cumplen. Incluso si lo hace bien, puede ser muy frágil, por ejemplo. reordenando llamadas a métodos de instancia u otras operaciones.
Los idiomas también varían en la forma en que resuelven las llamadas a los métodos de instancia que se heredan de las clases base / anuladas por las subclases, mientras el constructor todavía se está ejecutando. Esto agrega otra capa de complejidad.
Ejemplos específicos
Tu propio ejemplo de cómo crees que esto debería verse está equivocado:
esto no verifica el valor de retorno de
setAge
. Aparentemente llamar al setter no es garantía de corrección después de todo.Errores muy fáciles como depender del orden de inicialización, como:
donde mi registro temporal lo rompió todo. Whoops!
También hay lenguajes como C ++ donde llamar a un setter desde el constructor significa una inicialización predeterminada desperdiciada (que para algunas variables miembro al menos vale la pena evitar)
Una propuesta simple
Es cierto que la mayoría del código no está escrito así, pero si desea mantener su constructor limpio y predecible, y aún así reutilizar su lógica previa y posterior a la condición, la mejor solución es:
o incluso mejor, si es posible: codifique la restricción en el tipo de propiedad y haga que valide su propio valor en la asignación:
Y, por último, para completar, un contenedor autovalidado en C ++. Tenga en cuenta que aunque todavía está haciendo la tediosa validación, debido a que esta clase no hace nada más , es relativamente fácil verificar
OK, no está realmente completo, he omitido varios constructores y asignaciones de copiar y mover.
fuente
Cuando se construye un objeto, por definición no está completamente formado. Considere cómo el setter que proporcionó:
¿Qué sucede si parte de la validación
setAge()
incluye una verificación de laage
propiedad para garantizar que el objeto solo pueda aumentar en edad? Esa condición podría verse así:Parece un cambio bastante inocente, ya que los gatos solo pueden moverse a través del tiempo en una dirección. El único problema es que no puede usarlo para establecer el valor inicial de
this.age
porque supone quethis.age
ya tiene un valor válido.Evitar los setters en los constructores deja en claro que lo único que está haciendo el constructor es establecer la variable de instancia y omitir cualquier otro comportamiento que ocurra en el setter.
fuente
Algunas buenas respuestas aquí ya, pero hasta ahora nadie parecía haber notado que estás preguntando aquí dos cosas en una, lo que causa cierta confusión. En mi humilde opinión, tu publicación se ve mejor como dos preguntas separadas :
si hay una validación de una restricción en un setter, ¿no debería estar también en el constructor de la clase para asegurarse de que no se pueda omitir?
¿Por qué no llamar al constructor directamente para este propósito desde el constructor?
La respuesta a las primeras preguntas es claramente "sí". Un constructor, cuando no arroja una excepción, debe dejar el objeto en un estado válido, y si ciertos valores están prohibidos para ciertos atributos, entonces no tiene ningún sentido dejar que el ctor evite esto.
Sin embargo, la respuesta a la segunda pregunta suele ser "no", siempre y cuando no tome medidas para evitar anular el setter en una clase derivada. Por lo tanto, la mejor alternativa es implementar la validación de restricciones en un método privado que se pueda reutilizar tanto desde el setter como desde el constructor. No voy a repetir aquí el ejemplo @Useless, que muestra exactamente este diseño.
fuente
Aquí hay un poco de código Java que muestra los tipos de problemas con los que puede encontrarse utilizando métodos no finales en su constructor:
Cuando ejecuto esto, obtengo un NPE en el constructor NextProblem. Este es un ejemplo trivial, por supuesto, pero las cosas pueden complicarse rápidamente si tiene múltiples niveles de herencia.
Creo que la razón más importante por la que esto no se hizo común es porque hace que el código sea un poco más difícil de entender. Personalmente, casi nunca tengo métodos de establecimiento y mis variables miembro son casi siempre (juego de palabras) final. Por eso, los valores deben establecerse en el constructor. Si usa objetos inmutables (y hay muchas buenas razones para hacerlo) la pregunta es discutible.
En cualquier caso, es un buen objetivo reutilizar la validación u otra lógica y puede ponerla en un método estático e invocarla tanto desde el constructor como desde el instalador.
fuente
name
campo).this
referencia a algún otro método, porque aún no puede estar seguro de que esté completamente inicializado.¡Depende!
Si está realizando una validación simple o emitiendo efectos secundarios traviesos en el setter que necesitan ser alcanzados cada vez que se establece la propiedad: use el setter. La alternativa es generalmente extraer la lógica del setter del setter e invocar el nuevo método seguido por el set actual tanto del setter como del constructor, ¡lo cual es efectivamente lo mismo que usar el setter! (Excepto que ahora está manteniendo su setter de dos líneas, ciertamente pequeño, en dos lugares).
Sin embargo , si necesita realizar operaciones complejas para organizar el objeto antes de que un setter particular (¡o dos!) Se pueda ejecutar con éxito, no use el setter.
Y en cualquier caso, tenga casos de prueba . Independientemente de la ruta que tome, si tiene pruebas de unidad, tendrá una buena oportunidad de exponer los peligros asociados con cualquiera de las opciones.
También agregaré, si mi memoria de tan lejos es remotamente precisa, este es el tipo de razonamiento que también me enseñaron en la universidad. Y no está nada fuera de línea con proyectos exitosos en los que he tenido el privilegio de trabajar.
Si tuviera que adivinar, me perdí un memo de "código limpio" en algún momento que identificaba algunos errores típicos que pueden surgir del uso de setters en un constructor. Pero, por mi parte, no he sido víctima de tales errores ...
Por lo tanto, argumentaría que esta decisión en particular no necesita ser radical y dogmática.
fuente