java - valores - ¿Está bien exponer el estado de un objeto inmutable?
que significa inmutable en java (13)
Depende completamente de cómo va a usar el objeto. Los campos públicos no son inherentemente malvados, es malo dejar todo por defecto para que sea público. Por ejemplo, la clase java.awt.Point hace públicos sus campos xey, y ni siquiera son definitivos. Su ejemplo parece un buen uso de los campos públicos, pero, de nuevo, es posible que no desee exponer todos los campos internos de otro objeto inmutable. No hay una regla general.
Habiendo conocido recientemente el concepto de objetos inmutables, me gustaría conocer las mejores prácticas para controlar el acceso al estado. Aunque la parte orientada a objetos de mi cerebro hace que quiera acobardarme ante el miedo de los miembros públicos, no veo problemas técnicos con algo como esto:
public class Foo {
public final int x;
public final int y;
public Foo( int x, int y) {
this.x = x;
this.y = y;
}
}
Me sentiría más cómodo declarando los campos como private
y proporcionando métodos getter para cada uno, pero esto parece demasiado complejo cuando el estado es solo de lectura explícita.
¿Cuál es la mejor práctica para proporcionar acceso al estado de un objeto inmutable?
En los casos donde el único propósito es juntar dos valores bajo un nombre significativo, incluso puede considerar omitir la definición de cualquier constructor y mantener los elementos modificables:
public class Sculpture {
public int weight = 0;
public int price = 0;
}
Esto tiene la ventaja de minimizar el riesgo de confundir el orden de los parámetros al crear una instancia de la clase. La capacidad de cambio restringida, si es necesario, se puede lograr tomando todo el contenedor bajo control private
.
En realidad, rompe la encapsulación para exponer cualquier propiedad de un objeto de cualquier manera: cada propiedad es un detalle de implementación. El hecho de que todos lo hagan no lo hace correcto. El uso de accessors y mutators (getters y setters) no lo hace mejor. Por el contrario, los patrones CQRS se deben utilizar para mantener la encapsulación.
He pensado lo mismo en el pasado, pero generalmente termino haciendo que las variables sean privadas y usando getters y setters para que más adelante tenga la opción de realizar cambios en la implementación manteniendo la misma interfaz.
Esto me recordó algo que leí recientemente en "Código limpio" de Robert C. Martin. En el capítulo 6, él da una perspectiva ligeramente diferente. Por ejemplo, en la página 95 él declara
"Los objetos ocultan sus datos detrás de abstracciones y exponen las funciones que operan sobre esos datos. La estructura de los datos expone sus datos y no tienen funciones significativas".
Y en la página 100:
La cuasi encapsulación de los frijoles parece hacer que algunos puristas de OO se sientan mejor, pero por lo general no ofrece ningún otro beneficio.
Según la muestra del código, la clase Foo parecería ser una estructura de datos. Entonces, en base a lo que entendí de la discusión en Clean Code (que es más que solo las dos citas que di), el propósito de la clase es exponer datos, no funcionalidad, y tener getters y setters probablemente no sirva de mucho.
Una vez más, en mi experiencia, usualmente he seguido y usé el enfoque "frijol" de datos privados con getters y setters. Pero, de nuevo, nadie me pidió que escribiera un libro sobre cómo escribir mejor código, así que tal vez Martin tenga algo que decir.
Independientemente de la inmutabilidad, todavía estás exponiendo la implementación de esta clase. En algún momento querrá cambiar la implementación (o tal vez producir varias derivaciones, por ejemplo, utilizando el ejemplo Point, puede querer una clase Point similar utilizando coordenadas polares), y su código de cliente estará expuesto a esto.
El patrón anterior puede ser útil, pero generalmente lo restringiría a instancias muy localizadas (por ejemplo, pasando tuplas de información a mi alrededor; tiendo a encontrar que los objetos de información aparentemente no relacionada, sin embargo, son encapsulaciones incorrectas, o que la información es relacionado, y mi tupla se transforma en un objeto completamente desarrollado)
La clase que has desarrollado debe estar bien en su encarnación actual. Los problemas generalmente entran en juego cuando alguien intenta cambiar esta clase o heredar de ella.
Por ejemplo, después de ver el código anterior, alguien piensa en agregar otra instancia variable de miembro de la clase Bar.
public class Foo {
public final int x;
public final int y;
public final Bar z;
public Foo( int x, int y, Bar z) {
this.x = x;
this.y = y;
}
}
public class Bar {
public int age; //Oops this is not final, may be a mistake but still
public Bar(int age) {
this.age = age;
}
}
En el código anterior, la instancia de Bar no se puede cambiar, pero externamente, cualquiera puede actualizar el valor de Bar.age.
La mejor práctica es marcar todos los campos como privados, tener getters para los campos. Si devuelve un objeto o colección, asegúrese de devolver la versión no modificable.
La inmunidad es esencial para la programación simultánea.
Sé que solo un accesorio tiene captadores para las propiedades finales. Es el caso cuando desea tener acceso a las propiedades a través de una interfaz.
public interface Point {
int getX();
int getY();
}
public class Foo implements Point {...}
public class Foo2 implements Point {...}
De lo contrario, los campos finales públicos están bien.
Si su objeto tiene un uso lo suficientemente local como para no preocuparse por los problemas de la interrupción de los cambios de la API en el futuro, no hay necesidad de insertar getters en la parte superior de las variables de instancia. Pero este es un tema general, no específico de los objetos inmutables.
La ventaja de utilizar getters proviene de una capa adicional de direccionamiento indirecto, que puede ser útil si está diseñando un objeto que será ampliamente utilizado, y cuya utilidad se extenderá a un futuro imprevisible.
Solo quiero reflejar la reflection :
Foo foo = new Foo(0, 1); // x=0, y=1
Field fieldX = Foo.class.getField("x");
fieldX.setAccessible(true);
fieldX.set(foo, 5);
System.out.println(foo.x); // 5!
Entonces, ¿ Foo
es inmutable? :)
Un objeto con campos finales públicos que se cargan desde parámetros de constructores públicos se retrata de forma efectiva como un simple titular de datos. Si bien estos titulares de datos no son particularmente "OOP-ish", son útiles para permitir que un solo campo, variable, parámetro o valor de retorno encapsulen valores múltiples. Si el propósito de un tipo es servir como un medio simple de pegar unos pocos valores juntos, dicho titular de los datos suele ser la mejor representación en un marco sin tipos de valores reales.
Considere la pregunta de qué le gustaría que ocurra si algún método Foo
quiere darle a un llamador un Point3d
que encapsula "X = 5, Y = 23, Z = 57", y sucede que tiene una referencia a un Point3d
donde X = 5, Y = 23 y Z = 57. Si se sabe que Foo tiene un titular de datos inmutable, entonces Foo simplemente debe darle una referencia a la persona que llama. Sin embargo, si pudiera ser otra cosa (por ejemplo , podría contener información adicional más allá de X, Y y Z ), entonces Foo debería crear un nuevo titular de datos simple que contenga "X = 5, Y = 23, Z = 57" y dar la persona que llama es una referencia a eso.
Tener Point3d
sellado y exponer su contenido como campos finales públicos implicará que métodos como Foo pueden suponer que es un titular de datos inmutables simple y pueden compartir referencias de forma segura a las instancias de este. Si existe un código que haga tales suposiciones, puede ser difícil o imposible cambiar Point3d
para que sea algo más que un simple titular de datos inmutables sin romper dicho código. Por otro lado, el código que asume que Point3d
es un titular de datos inmutables simple puede ser mucho más simple y más eficiente que el código que tiene que tratar con la posibilidad de que sea otra cosa.
Usted ve este estilo mucho en Scala, pero hay una diferencia crucial entre estos lenguajes: Scala sigue el Principio de Acceso Uniforme , pero Java no. Eso significa que su diseño está bien siempre que su clase no cambie, pero puede romperse de varias maneras cuando necesite adaptar su funcionalidad:
- necesita extraer una interfaz o superclase (por ejemplo, su clase representa números complejos, y desea tener una clase de hermanos con representación de coordenadas polares, también)
- necesita heredar de su clase y la información se vuelve redundante (p. ej.,
x
puede calcularse a partir de datos adicionales de la subclase) - necesita probar las restricciones (por ejemplo,
x
debe ser no negativo por alguna razón)
También tenga en cuenta que no puede usar este estilo para miembros mutables (como el infame java.util.Date
). Solo con getters tienes la oportunidad de hacer una copia defensiva, o cambiar la representación (por ejemplo, almacenar la información de la Date
tanto long
)
Utilizo muchas construcciones muy similares a la que pones en la pregunta, a veces hay cosas que se pueden modelar mejor con un data-strcuture (a veces inmutable) que con una clase.
Todo depende, si está modelando un objeto, de un objeto definido por sus comportamientos, en este caso nunca exponga las propiedades internas. Otras veces está modelando una estructura de datos, y java no tiene una construcción especial para las estructuras de datos, está bien usar una clase y hacer públicas todas las propiedades, y si quiere que la inmutabilidad sea definitiva y pública fuera del curso.
Por ejemplo, Robert Martin tiene un capítulo sobre esto en el gran libro Clean Code, una lectura obligada en mi opinión.
Lo más importante a tener en cuenta es que las llamadas a funciones proporcionan una interfaz universal. Cualquier objeto puede interactuar con otros objetos mediante llamadas a funciones. Todo lo que tienes que hacer es definir las firmas correctas, y listo. La única pega es que tienes que interactuar únicamente a través de estas llamadas a funciones, que a menudo funcionan bien pero pueden ser torpes en algunos casos.
La razón principal para exponer directamente las variables de estado sería poder utilizar operadores primitivos directamente en estos campos. Cuando se hace bien, esto puede mejorar la legibilidad y la conveniencia: por ejemplo, agregar números complejos con +
o acceder a una colección con clave con []
. Los beneficios de esto pueden ser sorprendentes, siempre que su uso de la sintaxis siga las convenciones tradicionales.
El problema es que los operadores no son una interfaz universal. Solo un conjunto muy específico de tipos incorporados puede usarlos, estos solo pueden usarse de la manera que el lenguaje espera y no puede definir ninguno nuevo. Entonces, una vez que hayas definido tu interfaz pública usando primitivos, te has encerrado en el uso de esa primitiva, y solo esa primitiva (y otras cosas que se pueden convertir fácilmente en ella). Para usar cualquier otra cosa, debes bailar alrededor de ese primitivo cada vez que interactúas con él, y eso te mata desde una perspectiva SECA: las cosas pueden volverse muy frágiles muy rápidamente.
Algunos lenguajes convierten a los operadores en una interfaz universal, pero Java no. Esto no es una acusación de Java: sus diseñadores decidieron deliberadamente no incluir la sobrecarga del operador, y tenían buenas razones para hacerlo. Incluso cuando trabajas con objetos que parecen encajar bien con los significados tradicionales de los operadores, hacer que funcionen de una manera que realmente tenga sentido puede ser sorprendentemente matizado, y si no lo logras, vas a pagar por eso más tarde. A menudo es mucho más fácil hacer que una interfaz basada en funciones sea legible y utilizable que pasar por ese proceso, y a menudo incluso terminas con un resultado mejor que si hubieras utilizado operadores.
Sin embargo, hubo concesiones involucradas en esa decisión. Hay ocasiones en que una interfaz basada en el operador realmente funciona mejor que una basada en funciones, pero sin la sobrecarga del operador, esa opción simplemente no está disponible. Intentar calzar a los operadores de todos modos lo encerrará en algunas decisiones de diseño que probablemente no desee que sean inamovibles. Los diseñadores de Java pensaron que esta compensación valía la pena, e incluso podrían haber sido correctos al respecto. Pero decisiones como esta no vienen sin consecuencias, y este tipo de situación es donde golpean las consecuencias.
En resumen, el problema no es exponer su implementación, per se . El problema es encerrarse en esa implementación.