¿Está bien exponer el estado de un objeto inmutable?


Habiendo encontrado recientemente el concepto de objetos inmutables, me gustaría conocer las mejores prácticas para controlar el acceso al estado. A pesar de que la parte orientada a objetos de mi cerebro me hace querer encogerme de miedo a la vista 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 se lee explícitamente solo.

¿Cuál es la mejor práctica para proporcionar acceso al estado de un objeto inmutable?

Author: David Makogon, 2014-02-20

13 answers

Depende completamente de cómo vas a usar el objeto. Los campos públicos no son inherentemente malos, solo es malo predeterminar todo a ser público. Por ejemplo el java.awt.Point class hace que sus campos x e y sean públicos, y ni siquiera son finales. 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.

 62
Author: Kevin Workman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:33:36

He pensado lo mismo en el pasado, pero por lo general terminan haciendo las variables privadas y el uso de getters y setters para que más adelante voy a tener la opción de hacer cambios en la implementación manteniendo la misma interfaz.

Esto me recordó algo que leí recientemente en "Clean Code" de Robert C. Martin. En el capítulo 6 da una perspectiva ligeramente diferente. Por ejemplo, en la página 95 dice

" Los objetos ocultan sus datos detrás de abstracciones y exponen funciones que operan en esos datos. Estructura de datos exponer sus datos y no tienen funciones significativas."

Y en la página 100:

La cuasi-encapsulación de frijoles parece hacer que algunos puristas de OO se sientan mejor, pero generalmente no proporciona ningún otro beneficio.

Basado en el ejemplo de código, la clase Foo parecería ser una estructura de datos. Así que basado en lo que entendí de la discusión en Código Limpio (que es más que solo las dos citas que di), el propósito de la la clase es exponer datos, no funcionalidad, y tener getters y setters probablemente no hace mucho bien.

De nuevo, en mi experiencia, generalmente he seguido adelante y utilizado el enfoque "bean" 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.

 31
Author: Paul J Abernathy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:56:32

Si su objeto tiene un uso lo suficientemente local como para que no le importen los problemas de romper los cambios de API en el futuro, no es necesario agregar getters sobre las variables de instancia. Pero este es un tema general, no específico de objetos inmutables.

La ventaja de usar getters proviene de una capa adicional de indirección, que puede ser útil si está diseñando un objeto que será ampliamente utilizado, y cuya utilidad se extenderá a imprevisible futuro.

 11
Author: Marko Topolnik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:34:53

Independientemente de la inmutabilidad, todavía está exponiendo la implementación de esta clase. En algún momento querrá cambiar la implementación (o tal vez producir varias derivaciones, por ejemplo, usando el ejemplo de Punto, puede querer una clase de Punto similar usando coordenadas polares), y su código de cliente está expuesto a esto.

El patrón anterior bien puede ser útil, pero generalmente lo restringiría a instancias muy localizadas (por ejemplo, pasar tuplas de información alrededor-tiendo para encontrar que los objetos de información aparentemente no relacionados, sin embargo, o bien son malas encapsulaciones, o que la información está relacionada, y mi tupla se transforma en un objeto de pleno derecho)

 6
Author: Brian Agnew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-21 10:05:48

Lo importante a tener en cuenta es que las llamadas a funciones proporcionan una interfaz universal. Cualquier objeto puede interactuar con otros objetos usando llamadas a funciones. Todo lo que tienes que hacer es definir las firmas correctas, y listo. El único problema es que tienes que interactuar únicamente a través de estas llamadas de función, que a menudo funciona bien, pero puede ser torpe en algunos casos.

La razón principal para exponer variables de estado directamente sería ser capaz de utilizar operadores primitivos directamente en estos campos. Cuando se hace bien, esto puede mejorar la legibilidad y la comodidad: 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 se pueden usar de la manera que el lenguaje espera, y usted no puede define los nuevos. Y así, una vez que has definido tu interfaz pública usando primitivas, te has encerrado en usar esa primitiva, y solo esa primitiva (y otras cosas que pueden ser fácilmente lanzadas a ella). Para usar cualquier otra cosa, tienes que 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 es no es una acusación de Java: sus diseñadores eligieron deliberadamente no incluir la sobrecarga del operador, y tenían buenas razones para hacerlo. Incluso cuando estás trabajando con objetos que parecen encajar bien con los significados tradicionales de los operadores, hacerlos funcionar de una manera que realmente tenga sentido puede ser sorprendentemente matizado, y si no lo logras absolutamente, 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 eso proceso, y a menudo incluso terminan con un mejor resultado que si hubiera utilizado operadores.

Sin embargo, hubo compensaciones involucradas en esa decisión. Hay veces en que una interfaz basada en operadores realmente funciona mejor que una basada en funciones, pero sin sobrecarga del operador, esa opción simplemente no está disponible. Tratando de calzador operadores de todos modos se bloqueará en algunas decisiones de diseño que probablemente usted realmente no quiere ser inamovible. El Los diseñadores de Java pensaron que esta compensación valía la pena, e incluso podrían haber estado en lo cierto al respecto. Pero decisiones como esta no vienen sin algunas consecuencias, y este tipo de situación es donde las consecuencias golpean.

En resumen, el problema no es exponer su implementación, per se. El problema es encerrarse en esa implementación.

 5
Author: The Spooniest,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 18:17:43

En realidad, rompe la encapsulación para exponer cualquier propiedad de un objeto de cualquier manera every cada propiedad es un detalle de implementación. Sólo porque todo el mundo hace esto no lo hace correcto. El uso de accesores y mutadores (getters y setters) no lo hace mejor. Más bien, los patrones de CQRS deben usarse para mantener la encapsulación.

 4
Author: Engineer Dollery,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 21:11:11

Sé que solo un prop tiene getters 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.

 3
Author: AnatolyG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:49:34

La clase que has desarrollado, debería 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 de variable miembro de la barra de clase.

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.edad.

La mejor práctica es marcar todos los campos como privados, tener getters para los campos. Si está devolviendo un objeto o colección, asegúrese de devolver una versión no modificable.

La inmunatabilidad es esencial para la programación concurrente.

 3
Author: Shilpa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-26 01:29:00

Un objeto con campos finales públicos que se cargan de los parámetros del constructor público se retrata a sí mismo 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 encapsule múltiples valores. Si el propósito de un tipo es servir como un medio simple de pegar unos pocos valores juntos, tal titular de datos es a menudo la mejor representación en un marco sin valor real tipo.

Considerar la cuestión de lo que usted desea que suceda si algún método Foo quiere dar una llamada de un Point3d que encapsula "X=5, Y=23, Z=57", y se pasa a tener una referencia a un Point3d donde X=5, Y=23, y Z=57. Si se sabe que la cosa que tiene Foo es un simple titular de datos inmutables, entonces Foo simplemente debe darle a la persona que llama una referencia a ella. Si, sin embargo, podría ser algo más (por ejemplo, podría contener información adicional más allá de X, Y y Z), entonces Foo debe crear un nuevo titular de datos simple que contenga "X=5, Y=23, Z = 57" y darle al llamante una referencia a eso.

Tener Point3d sellado y exponer su contenido como campos finales públicos implicará que métodos como Foo pueden asumir que es un simple titular de datos inmutables y pueden compartir con seguridad referencias a instancias de la misma. Si existe código que hace tales suposiciones, puede ser difícil o imposible cambiar Point3d para ser cualquier cosa que no sea un simple titular de datos inmutables sin romper tales codificar. Por otro lado, el código que asume que Point3d es un simple titular de datos inmutables puede ser mucho más simple y más eficiente que el código que tiene que lidiar con la posibilidad de que sea otra cosa.

 2
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-21 18:30:39

Se ve mucho este estilo 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 y cuando su clase no cambie, pero puede romperse de varias maneras cuando necesita adaptar su funcionalidad:

  • necesita extraer una interfaz o superclase (por ejemplo, su clase representa números complejos, y desea tener una clase hermana con coordenadas polares representación, también)
  • necesita heredar de su clase, y la información se vuelve redundante (por ejemplo, x se puede calcular a partir de datos adicionales de la subclase)
  • debe 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 de cambiar la representación (por ejemplo, almacenar el Date información como long)

 2
Author: Landei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-22 19:14:01

Utilizo muchas construcciones muy similares a la que pones en la pregunta, a veces hay cosas que se pueden modelar mejor con una (a veces inmutable) data-strcuture que con una clase.

Todo depende, si está modelando un objeto, un objeto está definido por sus comportamientos, en este caso nunca exponga propiedades internas. Otras veces está modelando una estructura de datos, y java no tiene una construcción especial para estructuras de datos, está bien usar una clase y hacer pública toda la propiedades, y si desea inmutabilidad final y público fuera de curso.

Por ejemplo, Robert Martin tiene un capítulo sobre esto en el gran libro Clean Code, a must read in my opinion.

 1
Author: AlfredoCasado,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:38:28

En los casos en que el único propósito es acoplar dos valores entre sí bajo un nombre significativo, incluso puede considerar omitir la definición de cualquier constructor y mantener los elementos cambiables:

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 instanciar la clase. La capacidad de cambio restringida, en caso necesario, puede lograrse controlando todo el contenedor private.

 1
Author: Wolf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-26 09:40:58

Solo quiero reflejar reflexión :

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, ¿es Foo todavía inmutable? :)

 0
Author: bobbel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 14:45:58