¿Qué debe permitirse dentro de getters y setters?

45

Me metí en un interesante argumento de Internet sobre los métodos get y setter y la encapsulación. Alguien dijo que todo lo que deberían hacer es una asignación (establecedores) o un acceso variable (captadores) para mantenerlos "puros" y garantizar la encapsulación.

  • ¿Estoy en lo cierto de que esto derrotaría por completo el propósito de tener captadores y establecedores en primer lugar y debería permitirse la validación y otra lógica (sin efectos secundarios extraños, por supuesto)?
  • ¿Cuándo debe ocurrir la validación?
    • Al establecer el valor, dentro del setter (para proteger el objeto de entrar en un estado no válido, mi opinión)
    • Antes de establecer el valor, fuera del setter
    • Dentro del objeto, antes de cada vez que se usa el valor
  • ¿Se permite a un setter cambiar el valor (tal vez convertir un valor válido en alguna representación interna canónica)?
Botá Balázs
fuente
18
Lo mejor de los getters y setters es la capacidad de no tenerlos , es decir, dejar el setter y tener una propiedad de solo lectura, salir del getter y tener la opción de configuración cuyo valor actual no es asunto de nadie.
Michael Borgwardt
77
@MichaelBorgwardt Deje a los dos fuera para tener una interfaz limpia "decir, no preguntar". Propiedades = código potencial olor.
Konrad Rudolph el
2
Los setters también se pueden usar para generar un evento .
John Isaiah Carmona
@KonradRudolph Estoy de acuerdo con su declaración, aunque me gustaría enfatizar realmente la palabra "potencial".
Phil

Respuestas:

37

Recuerdo haber tenido una discusión similar con mi profesor cuando aprendí C ++ en la universidad. Simplemente no podía ver el punto de usar getters y setters cuando podía hacer pública una variable. Ahora entiendo mejor con años de experiencia y he aprendido una razón mejor que simplemente decir "mantener la encapsulación".

Al definir los captadores y establecedores, proporcionará una interfaz coherente para que si desea cambiar su implementación, sea menos probable que rompa el código dependiente. Esto es especialmente importante cuando sus clases están expuestas a través de una API y se utilizan en otras aplicaciones o por terceros. Entonces, ¿qué pasa con las cosas que entran en el getter o setter?

Los captadores generalmente están mejor implementados como un simple paso hacia abajo para acceder a un valor porque esto hace que su comportamiento sea predecible. Digo en general, porque he visto casos en los que los captadores se han utilizado para acceder a valores manipulados por cálculo o incluso por código condicional. En general, no es tan bueno si está creando componentes visuales para usar en tiempo de diseño, pero aparentemente es útil en tiempo de ejecución. Sin embargo, no existe una diferencia real entre esto y el uso de un método simple, excepto que cuando usa un método, generalmente es más probable que nombre un método de manera más apropiada para que la funcionalidad del "getter" sea más evidente al leer el código.

Compare lo siguiente:

int aValue = MyClass.Value;

y

int aValue = MyClass.CalculateValue();

La segunda opción deja en claro que el valor se está calculando, mientras que el primer ejemplo le dice que simplemente está devolviendo un valor sin saber nada sobre el valor en sí.

Quizás podría argumentar que lo siguiente sería más claro:

int aValue = MyClass.CalculatedValue;

Sin embargo, el problema es que está asumiendo que el valor ya ha sido manipulado en otro lugar. Por lo tanto, en el caso de un captador, si bien es posible que desee suponer que algo más podría estar sucediendo cuando devuelve un valor, es difícil aclarar esas cosas en el contexto de una propiedad, y los nombres de las propiedades nunca deben contener verbos de lo contrario, es difícil entender de un vistazo si el nombre utilizado debe estar decorado con paréntesis cuando se accede.

Sin embargo, los setters son un caso ligeramente diferente. Es completamente apropiado que un establecedor proporcione un procesamiento adicional para validar los datos que se envían a una propiedad, lanzando una excepción si establecer un valor violaría los límites definidos de la propiedad. Sin embargo, el problema que tienen algunos desarrolladores al agregar procesamiento a los establecedores es que siempre existe la tentación de que el colocador haga un poco más, como realizar un cálculo o una manipulación de los datos de alguna manera. Aquí es donde puede obtener efectos secundarios que en algunos casos pueden ser impredecibles o indeseables.

En el caso de los setters, siempre aplico una regla general simple, que es hacer lo menos posible a los datos. Por ejemplo, generalmente permitiré la prueba de límites y el redondeo para que pueda plantear excepciones si es apropiado, o evitar excepciones innecesarias donde puedan evitarse de manera sensata. Las propiedades de punto flotante son un buen ejemplo en el que es posible que desee redondear los decimales excesivos para evitar generar una excepción, al tiempo que permite ingresar valores de rango con algunos decimales adicionales.

Si aplica algún tipo de manipulación de la entrada del setter, tiene el mismo problema que con el getter, que es difícil permitir que otros sepan lo que está haciendo el setter simplemente nombrándolo. Por ejemplo:

MyClass.Value = 12345;

¿Le dice esto algo sobre lo que va a pasar con el valor cuando se le da al setter?

Qué tal si:

MyClass.RoundValueToNearestThousand(12345);

El segundo ejemplo le dice exactamente qué va a pasar con sus datos, mientras que el primero no le permitirá saber si su valor se modificará arbitrariamente. Al leer el código, el segundo ejemplo será mucho más claro en su propósito y función.

¿Estoy en lo cierto de que esto derrotaría por completo el propósito de tener captadores y establecedores en primer lugar y debería permitirse la validación y otra lógica (sin efectos secundarios extraños, por supuesto)?

Tener getters y setters no se trata de encapsular en aras de la "pureza", sino de encapsular para permitir que el código se refactorice fácilmente sin arriesgarse a un cambio en la interfaz de la clase que de lo contrario rompería la compatibilidad de la clase con el código de llamada. La validación es totalmente apropiada en un setter, sin embargo, existe un pequeño riesgo de que un cambio en la validación pueda romper la compatibilidad con el código de llamada si el código de llamada se basa en la validación que ocurre de una manera particular. Esta es una situación generalmente rara y de riesgo relativamente bajo, pero debe tenerse en cuenta en aras de la integridad.

¿Cuándo debe ocurrir la validación?

La validación debe realizarse dentro del contexto del establecedor antes de establecer realmente el valor. Esto garantiza que si se produce una excepción, el estado de su objeto no cambiará y potencialmente invalidará sus datos. En general, me parece mejor delegar la validación a un método separado que sería lo primero que se llama dentro del configurador, para mantener el código del configurador relativamente ordenado.

¿Se permite a un setter cambiar el valor (tal vez convertir un valor válido en alguna representación interna canónica)?

En casos muy raros, tal vez. En general, probablemente sea mejor no hacerlo. Este es el tipo de cosas que mejor se deja a otro método.

S.Robins
fuente
re: cambie el valor, podría ser razonable establecerlo en -1 o algún token de bandera NULL si se le pasa un valor ilegal al colocador
Martin Beckett
1
Hay un par de problemas con esto. Establecer un valor arbitrariamente crea un efecto secundario deliberado y poco claro. Además, no permite que el código de llamada reciba comentarios que podrían usarse para tratar mejor los datos ilegales. Esto es particularmente importante con valores en el nivel de IU. Sin embargo, para ser justos, una excepción que pensé podría ser si permitía múltiples formatos de fecha como entrada, mientras almacenaba la fecha en un formato estándar de fecha / hora. Se podría argumentar que este "efecto secundario" particular es una normalización de los datos en la validación, siempre que los datos de entrada sean legales.
S.Robins
Sí, una de las justificaciones de un establecedor es que debe devolver verdadero / falso si se puede establecer el valor.
Martin Beckett
1
"Las propiedades de coma flotante son un buen ejemplo en el que es posible que desee redondear decimales excesivos para evitar generar una excepción" ¿En qué circunstancia tener demasiados decimales genera una excepción?
Mark Byers
2
Veo cambiar el valor a una representación canónica como una forma de validación. Valores predeterminados que faltan (p. Ej., La fecha actual si una marca de tiempo contiene solo la hora) o escalar un valor (.93275 -> 93.28%) para garantizar la coherencia interna debe estar bien, pero cualquier manipulación de este tipo debe mencionarse explícitamente en la documentación de la API , especialmente si son un idioma poco común dentro de la API.
TMN
20

Si el getter / setter simplemente refleja el valor, entonces no tiene sentido tenerlos o hacer que el valor sea privado. No tiene nada de malo hacer públicas algunas variables miembro si tiene una buena razón. Si está escribiendo una clase de puntos 3d, entonces tener público .x, .y, .z tiene mucho sentido.

Como dijo Ralph Waldo Emerson: "Una consistencia tonta es el duende de las mentes pequeñas, adoradas por los pequeños estadistas y filósofos y los diseñadores de Java".

Los captadores / establecedores son útiles donde puede haber efectos secundarios, donde necesita actualizar otras variables internas, recalcular valores almacenados en caché y proteger la clase de entradas no válidas.

La justificación habitual para ellos, que ocultan la estructura interna, es generalmente la menos útil. p.ej. Tengo estos puntos almacenados como 3 flotantes, podría decidir almacenarlos como cadenas en una base de datos remota, así que haré getters / setters para ocultarlos, como si pudieras hacer eso sin tener ningún otro efecto en el código de la persona que llama.

Martin Beckett
fuente
1
Wow, los diseñadores de Java son divinos? </jk>
@delnan, obviamente no, o rimarían mejor ;-)
Martin Beckett
¿Sería válido crear un punto 3d en el que x, y, z sean todos Float.NAN?
Andrew T Finnell el
44
No estoy de acuerdo con su punto de que "la justificación habitual" (ocultar la estructura interna) es la propiedad menos útil de los captadores y establecedores. En C #, definitivamente es útil hacer que la propiedad sea una interfaz cuya estructura subyacente se puede cambiar; por ejemplo, si solo desea una propiedad disponible para enumeración, es mucho mejor decir IEnumerable<T>que forzarla a algo así List<T>. Diría que su ejemplo de acceso a la base de datos está violando una responsabilidad única: mezclar la representación del modelo con la forma en que persiste.
Brandon Linton
@AndrewFinnell: podría ser una buena manera de marcar puntos como imposibles / no válidos / eliminados, etc.
Martin Beckett
8

Principio de acceso uniforme de Meyer: "Todos los servicios ofrecidos por un módulo deben estar disponibles a través de una notación uniforme, que no traicione si se implementan a través del almacenamiento o la computación". es la razón principal detrás de los captadores / establecedores, también conocidos como Propiedades.

Si decide almacenar en caché o calcular con pereza un campo de una clase, puede cambiar esto en cualquier momento si solo ha expuesto los accesores de propiedad y no los datos concretos.

Los objetos de valor, las estructuras simples no necesitan esta abstracción, pero una clase completa, en mi opinión.

Karl
fuente
2

Una estrategia común para diseñar clases, que se introdujo a través del lenguaje Eiffel, es la Separación de consulta de comandos . La idea es que un método debe o bien que decir algo sobre el objeto o indicar al objeto de hacer algo, pero no hacer las dos cosas.

Esto solo se relaciona con la interfaz pública de la clase, no con la representación interna. Considere un objeto de modelo de datos respaldado por una fila en la base de datos. Puede crear el objeto sin cargar los datos, luego, la primera vez que llama a un captador, realmente lo hace SELECT. Está bien, es posible que esté cambiando algunos detalles internos sobre cómo se representa el objeto, pero no está cambiando la forma en que aparece para los clientes de ese objeto. Debería poder llamar a los captadores varias veces y seguir obteniendo los mismos resultados, incluso si realizan un trabajo diferente para devolver esos resultados.

De manera similar, un setter se ve, contractualmente, como si solo cambiara el estado de un objeto. Podría hacerlo de una manera enrevesada: escribir un archivo UPDATEen una base de datos o pasar el parámetro a algún objeto interno. Eso está bien, pero sería sorprendente hacer algo que no esté relacionado con la configuración del estado.

Meyer (el creador de Eiffel) también tenía cosas que decir sobre la validación. Esencialmente, cada vez que un objeto está inactivo debe estar en un estado válido. Entonces, justo después de que el constructor haya terminado, antes y después (pero no necesariamente durante) cada llamada a un método externo, el estado del objeto debe ser consistente.

Es interesante notar que en ese lenguaje, la sintaxis para llamar a un procedimiento y para leer un atributo expuesto se ve igual. En otras palabras, la persona que llama no puede saber si está usando algún método o trabajando directamente con una variable de instancia. Es solo en idiomas que no ocultan este detalle de implementación donde surge la pregunta: si las personas que llaman no pueden decirlo, puede cambiar entre un ivar público y accesores sin filtrar ese cambio en el código del cliente.


fuente
1

Otra cosa aceptable para hacer es la clonación. En algunos casos, debe asegurarse de que después de que alguien le dé su clase, por ejemplo. una lista de algo, él no puede cambiarlo dentro de su clase (ni cambiar el objeto en esa lista). Por lo tanto, realiza una copia profunda del parámetro en setter y devuelve una copia profunda en getter. (El uso de tipos inmutables como parámetros es otra opción, pero lo anterior supone que no es posible) Pero no clone en accesores si no es necesario. Es fácil pensar (pero no de manera adecuada) acerca de las propiedades / captadores y establecedores como operaciones de costo constante, por lo que este es el rendimiento de las minas terrestres esperando al usuario de la API.

user470365
fuente
1

No siempre hay un mapeo 1-1 entre los accesos de propiedad y los ivars que almacenan los datos.

Por ejemplo, una clase de vista puede proporcionar una centerpropiedad aunque no haya ivar que almacene el centro de la vista; la configuración centerprovoca cambios en otros ivars, como origino transformlo que sea, pero los clientes de la clase no saben ni les importa cómo center se almacenan, siempre que funcionen correctamente. Sin embargo, lo que no debería suceder es que la configuración centerhace que las cosas sucedan más allá de lo necesario para guardar el nuevo valor, sin embargo, eso se hace.

Caleb
fuente
0

La mejor parte de los setters y getters es que facilitan alterar las reglas de una API sin cambiar la API. Si detecta un error, es mucho más probable que pueda corregir el error en la biblioteca y no hacer que cada consumidor actualice su base de código.

AlexanderBrevig
fuente
-4

Tiendo a creer que los setters y getters son malvados y solo deberían usarse en clases administradas por un framework / contenedor. Un diseño de clase adecuado no debería producir captadores y colocadores.

Editar: un artículo bien escrito sobre este tema .

Edit2: los campos públicos son una tontería en un enfoque OOP; Al decir que los captadores y los colocadores son malvados, no quiero decir que deberían ser reemplazados por campos públicos.

m3th0dman
fuente
1
Creo que te estás perdiendo el punto del artículo, que sugiere usar getters / setters con moderación y evitar exponer los datos de la clase a menos que sea necesario. Este IMHO es un principio de diseño sensato que el desarrollador debe aplicar deliberadamente. En términos de crear propiedades en una clase, simplemente podría exponer una variable, pero esto hace que sea más difícil proporcionar validación cuando se establece la variable, o extraer el valor de una fuente alternativa cuando se "obtiene", esencialmente violando la encapsulación, y potencialmente encerrándolo en un diseño de interfaz difícil de mantener.
S.Robins
@ S.Robins En mi opinión, los captadores y establecedores públicos están equivocados; los campos públicos están más equivocados (si eso es comparable).
m3th0dman
@methodman Sí, estoy de acuerdo en que un campo público está mal, sin embargo, una propiedad pública puede ser útil. Esa propiedad se puede usar para proporcionar un lugar para la validación o eventos relacionados con la configuración o la devolución de los datos, según el requisito específico en ese momento. Los captadores y establecedores en sí mismos no están equivocados per se. Por otro lado, cómo y cuándo se usan o se abusa de ellos puede verse como pobre en términos de diseño y mantenibilidad según las circunstancias. :)
S.Robins
@ S.Robins Piense en los conceptos básicos de OOP y modelado; Se supone que los objetos copian entidades de la vida real. ¿Existen entidades de la vida real que tienen este tipo de operaciones / propiedades como getters / setters?
m3th0dman
Para evitar tener demasiados comentarios aquí, llevaré esta conversación a este chat y abordaré tu comentario allí.
S.Robins