¿Es un antipatrón si una propiedad de clase crea y devuelve una nueva instancia de una clase?

24

Tengo una clase llamada Headingque hace algunas cosas, pero también debería ser capaz de devolver el opuesto del valor del título actual, que finalmente debe usarse mediante la creación de una nueva instancia de la Headingclase en sí.

Puedo tener una propiedad simple llamada reciprocalpara devolver el encabezado opuesto del valor actual y luego crear manualmente una nueva instancia de la clase de Encabezado, o puedo crear un método como createReciprocalHeading()crear automáticamente una nueva instancia de la clase de Encabezado y devolverlo al usuario.

Sin embargo, uno de mis colegas me recomendó crear una propiedad de clase llamada reciprocalque devuelva una nueva instancia de la clase a través de su método getter.

Mi pregunta es: ¿no es un antipatrón que una propiedad de una clase se comporte así?

Particularmente encuentro esto menos intuitivo porque:

  1. En mi opinión, una propiedad de una clase no debería devolver una nueva instancia de una clase, y
  2. El nombre de la propiedad, que es reciprocal, no ayuda al desarrollador a comprender completamente su comportamiento, sin obtener ayuda del IDE o verificar la firma del captador.

¿Estoy siendo demasiado estricto sobre lo que debe hacer una propiedad de clase o es una preocupación válida? Siempre he tratado de administrar el estado de la clase a través de sus campos y propiedades y su comportamiento a través de sus métodos, y no veo cómo esto se ajusta a la definición de la propiedad de la clase.

53777A
fuente
66
Como @Ewan dice en el último párrafo de su respuesta, lejos de ser un antipatrón, tener Headingun tipo inmutable y reciprocaldevolver una nueva Headinges una buena práctica de "pozo de éxito". (con la advertencia de que las dos llamadas reciprocaldeben devolver "lo mismo", es decir, deben pasar la prueba de igualdad).
David Arno,
20
"mi clase no es inmutable". Ahí está tu verdadero antipatrón. ;)
David Arno
3
@DavidArno Bueno, a veces hay una gran penalización de rendimiento por hacer que ciertas cosas sean inmutables, especialmente si el lenguaje no lo admite de forma nativa, pero de lo contrario estoy de acuerdo en que la inmutabilidad es una buena práctica en muchos casos.
53777A
2
@dcorking la clase se implementa tanto en C # como en TypeScript, pero prefiero mantener este lenguaje agnóstico.
53777A
2
@Kaiserludi suena como una respuesta (una gran respuesta) - los comentarios son para solicitar claridad
dcorking

Respuestas:

22

No es desconocido tener cosas como Copy () o Clone () pero sí, creo que tienes razón en preocuparte por esto.

tomar como ejemplo:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Sería bueno tener alguna advertencia de que la propiedad era un objeto nuevo cada vez.

También puede suponer que:

h2.reciprocal == h1

Sin embargo. Si su clase de encabezado fuera un tipo de valor inmutable, entonces podría implementar estas relaciones y recíproco podría ser un buen nombre para la operación

Ewan
fuente
1
Solo tenga en cuenta que Copy()y Clone()son métodos, no propiedades. Creo que el OP se refiere a los captadores de propiedades que devuelven nuevas instancias.
Lynn
66
Lo sé. pero las propiedades son (más o menos) también métodos en c #
Ewan
44
En ese caso, C # tiene mucho más que ofrecer: String.Substring(), Array.Reverse(), Int32.GetHashCode(), etc.
Lynn
If your heading class was an immutable value type- Creo que debería agregar que los establecedores de propiedades en objetos inmutables deberían devolver siempre las nuevas instancias (o al menos si el nuevo valor no es el mismo que el valor anterior), porque esa es la definición de objetos inmutables. :)
Ivan Kolmychek
17

Una interfaz de una clase lleva al usuario de la clase a hacer suposiciones sobre cómo funciona.

Si muchos de estos supuestos son correctos y pocos son incorrectos, la interfaz es buena.

Si muchos de estos supuestos son incorrectos y pocos son correctos, la interfaz es basura.

Una suposición común sobre las Propiedades es que llamar a la función get es barato. Otra suposición común sobre las propiedades es que llamar a la función get dos veces seguidas devolverá lo mismo.


Puede solucionar este problema utilizando la coherencia para cambiar las expectativas. Por ejemplo, para una pequeña biblioteca 3D donde necesita Vector, Ray Matrixetc., puede hacer que los captadores como Vector.normaly Matrix.inversesean casi siempre caros. Desafortunadamente, incluso si sus interfaces usan constantemente propiedades costosas, las interfaces serán, en el mejor de los casos, tan intuitivas como las que usa Vector.create_normal()y Matrix.create_inverse(), sin embargo, no conozco ningún argumento sólido que pueda hacerse que el uso de propiedades crea una interfaz más intuitiva, incluso después de cambiar las expectativas.

Peter - Unban Robert Harvey
fuente
10

Las propiedades deberían regresar muy rápidamente, devolver el mismo valor con llamadas repetidas y obtener su valor no debería tener efectos secundarios. Lo que describe aquí no debe implementarse como una propiedad.

Su intuición es ampliamente correcta. Un método de fábrica no devuelve el mismo valor con llamadas repetidas. Devuelve una nueva instancia cada vez. Esto prácticamente lo descalifica como una propiedad. Tampoco es rápido si el desarrollo posterior agrega peso a la creación de la instancia, por ejemplo, dependencias de la red.

Las propiedades generalmente deben ser operaciones muy simples que obtienen / establecen una parte del estado actual de la clase. Las operaciones tipo fábrica no cumplen con este criterio.

Brad Thomas
fuente
1
La DateTime.Nowpropiedad se usa comúnmente y se refiere a un nuevo objeto. Creo que el nombre de una propiedad puede ayudar a eliminar la ambigüedad sobre si se devuelve o no el mismo objeto cada vez. Todo está en el nombre y cómo se usa.
Greg Burghardt
3
DateTime.Now se considera un error. Ver stackoverflow.com/questions/5437972/…
Brad Thomas el
@GregBurghardt El efecto secundario de un nuevo objeto podría no ser un problema en sí mismo, porque DateTimees un tipo de valor y no tiene un concepto de identidad de objeto. Si entiendo correctamente, el problema es que no es determinista y devuelve un nuevo valor cada vez.
Zev Spitz
1
@GregBurghardt DateTime.Newes estático y, por lo tanto, no puede esperar que represente el estado de un objeto.
Tsahi Asher
5

No creo que haya una respuesta agnóstica del idioma a esto, ya que lo que constituye una "propiedad" es una pregunta específica del idioma, y ​​lo que espera una persona que llama de una "propiedad" también es una pregunta específica del idioma. Creo que la forma más fructífera de pensar en esto es pensar en cómo se ve desde el punto de vista de la persona que llama.

En C #, las propiedades se distinguen porque están capitalizadas (convencionalmente) (como los métodos) pero carecen de paréntesis (como las variables de instancia pública). Si ve el siguiente código, sin documentación, ¿qué espera?

var reciprocalHeading = myHeading.Reciprocal;

Como un novato relativo de C #, pero uno que ha leído las Directrices de uso de propiedades de Microsoft , esperaría Reciprocal, entre otras cosas:

  1. ser un miembro de datos lógico de la Headingclase
  2. ser económico para llamar, de modo que no sea necesario que guarde en caché el valor
  3. falta de efectos secundarios observables
  4. producir el mismo resultado si se llama dos veces seguidas
  5. (tal vez) ofrecer un ReciprocalChangedevento

De estos supuestos, (3) y (4) son probablemente correctos (suponiendo que Headinges un tipo de valor inmutable, como en la respuesta de Ewan ), (1) es discutible, (2) es desconocido pero también discutible, y (5) es poco probable que tiene sentido semántico (aunque cualquier cosa que tenga un encabezado quizás debería tener un HeadingChangedevento). Esto me sugiere que en una API de C #, "obtener o calcular el recíproco" no debe implementarse como una propiedad, pero especialmente si el cálculo es barato e Headinginmutable, es un caso límite.

(Sin embargo, tenga en cuenta que ninguna de estas preocupaciones tiene nada que ver con si llamar a la propiedad crea una nueva instancia , ni siquiera (2). Crear objetos en el CLR, en sí mismo, es bastante barato).

En Java, las propiedades son una convención de nomenclatura de métodos. Si yo veo

Heading reciprocalHeading = myHeading.getReciprocal();

mis expectativas son similares a las anteriores (si se establecen de manera menos explícita): espero que la llamada sea barata, idempotente y carente de efectos secundarios. Sin embargo, fuera del marco de JavaBeans, el concepto de una "propiedad" no es tan significativo en Java, y particularmente cuando se considera una propiedad inmutable sin correspondencia setReciprocal(), la getXXX()convención está algo anticuada. Desde Effective Java , segunda edición (ya tiene más de ocho años):

Los métodos que devuelven una no booleanfunción o atributo del objeto en el que se invocan generalmente se nombran con un sustantivo, una frase nominal o una frase verbal que comience con el verbo get…. Hay un contingente vocal que afirma que solo la tercera forma (comenzando con get) es aceptable, pero hay poca base para esta afirmación. Las dos primeras formas generalmente conducen a un código más legible ... (p. 239)

En una API contemporánea y más fluida, entonces, esperaría ver

Heading reciprocalHeading = myHeading.reciprocal();

- lo que nuevamente sugeriría que la llamada es barata, idempotente y carece de efectos secundarios, pero no diría nada sobre si se realiza un nuevo cálculo o si se crea un nuevo objeto. Esto esta bien; en una buena API, no me debería importar.

En Ruby, no existe tal cosa como una propiedad. Hay "atributos", pero si veo

reciprocalHeading = my_heading.reciprocal

No tengo forma inmediata de saber si estoy accediendo a una variable de instancia a @reciprocaltravés de un attr_readermétodo simple o de acceso, o si estoy llamando a un método que realiza un cálculo costoso. Sin embargo, el hecho de que el nombre del método sea un nombre simple calcReciprocalsugiere, una vez más, que la llamada es al menos barata y probablemente no tiene efectos secundarios.

En Scala, la convención de nomenclatura es que los métodos con efectos secundarios toman paréntesis y los métodos sin ellos no, pero

val reciprocal = heading.reciprocal

podría ser cualquiera de:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Tenga en cuenta que Scala permite varias cosas que, sin embargo, no se recomiendan en la guía de estilo . Esta es una de mis principales molestias con Scala).

La falta de paréntesis me dice que la llamada no tiene efectos secundarios; el nombre, nuevamente, sugiere que la llamada debería ser relativamente barata. Más allá de eso, no me importa cómo me da el valor.

En resumen: conozca el lenguaje que está utilizando y sepa qué expectativas traerán otros programadores a su API. Todo lo demás es un detalle de implementación.

David Moles
fuente
1
Haz +1 en una de mis respuestas favoritas, gracias por tomarte tu tiempo para escribir esto.
53777A
2

Como han dicho otros, es un patrón bastante común devolver instancias de la misma clase.

La nomenclatura debe ir de la mano con las convenciones de nomenclatura del lenguaje.

Por ejemplo, en Java probablemente esperaría que se llamara getReciprocal();

Dicho esto, consideraría objetos mutables vs inmutables .

Con los inmutables , las cosas son bastante fáciles, y no dolerá si devuelve el mismo objeto o no.

Con los mutables , esto puede ser muy aterrador.

b = a.reciprocal
a += 1
b = what?

¿A qué se refiere b? ¿El recíproco del valor original de a? ¿O el recíproco del cambiado? Este podría ser un ejemplo demasiado corto, pero entiendes el punto.

En esos casos, busque un mejor nombre que comunique lo que sucede, por ejemplo, createReciprocal()podría ser una mejor opción.

Pero realmente también depende del contexto.

Eiko
fuente
¿ Quisiste decir mutable ?
Carsten S
@CarstenS Sí, por supuesto, gracias. No tengo idea de dónde estaba mi cabeza. :)
Eiko
No estoy de acuerdo getReciprocal(), ya que "suena" como un captador de estilo JavaBeans "normal". Prefiere "createXXX ()" o posiblemente "CalculateXXX ()" que indica o insinúa a otros programadores que algo diferente está sucediendo
user949300
@ user949300 estoy de acuerdo tanto con sus preocupaciones - y mencionó el nombre de ... crear más abajo. Sin embargo, también hay inconvenientes. crear ... implica nuevas instancias que pueden no ser siempre el caso (piense en objetos dedicados singulares para algunos casos especiales), calcule ... una especie de detalles de implementación de fugas, lo que podría alentar suposiciones incorrectas en la etiqueta de precio.
Eiko
1

En aras de la responsabilidad única y la claridad, tendría una ReverseHeadingFactory que tomó el objeto y devolvió su inverso. Esto aclararía que se estaba devolviendo un nuevo objeto y significaría que el código para producir el reverso estaba encapsulado lejos de otro código.

monstruo mate
fuente
1
+1 para la claridad del nombre, pero ¿qué tiene de malo SRP en otras soluciones?
53777A
3
¿Por qué recomienda una clase de fábrica sobre un método de fábrica? (En mi opinión, una responsabilidad útil de un objeto es a menudo a los objetos emiten que son como propio, como iterator.next ¿Esta leve violación de SRP causa otros problemas de ingeniería de software.?)
dcorking
2
@dcorking Una clase de fábrica versus un método (en mi opinión) suele ser el mismo tipo de pregunta que "¿El objeto es comparable o debería hacer un comparador?" Siempre está bien hacer un comparador, pero solo está bien hacerlos comparables si es una forma bastante universal de compararlos. (Por ejemplo, los números más grandes son mayores que los más pequeños, esto es bueno para comparables, pero se podría decir que todas las igualaciones son mayores que todas las probabilidades, lo convertiría en un comparador). Entonces, para esto, creo que solo hay uno manera de revertir un encabezado, por lo que me inclinaría por hacer un método para evitar una clase adicional.
Capitán Man
0

Depende. Por lo general, espero que las propiedades devuelvan cosas que son parte de una instancia, por lo que devolver una instancia diferente de la misma clase sería un poco inusual.

Pero, por ejemplo, una clase de cadena podría tener propiedades "firstWord", "lastWord", o si maneja Unicode "firstLetter", "lastLetter", que serían objetos de cadena completa, por lo general, los más pequeños.

gnasher729
fuente
0

Como las otras respuestas cubren la parte de Propiedad, solo abordaré su # 2 sobre reciprocal:

No use nada más que reciprocal. Es el único término correcto para lo que está describiendo (y es el término formal). No escriba incorrecto del software para guardar los desarrolladores de aprender el dominio que se está trabajando. En la navegación, los términos tienen significados muy específicos y el uso de algo tan aparentemente inocuo como reverseo oppositepodría dar lugar a confusión en ciertos contextos.

Shaz
fuente
0

Lógicamente, no, no es propiedad de un encabezado. Si lo fuera, también se podría decir que un número negativo es propiedad de ese número y, francamente, eso haría que "propiedad" pierda todo significado, casi todo sería una propiedad.

Recíproco es una función pura de un encabezado, lo mismo que negativo de un número es una función pura de ese número.

Como regla general, si configurarlo no tiene sentido, probablemente no debería ser una propiedad. Definirlo todavía se puede rechazar, por supuesto, pero agregar un setter debería ser teóricamente posible y significativo.

Ahora, en algunos idiomas, podría tener sentido tenerlo como una propiedad como se especifica en el contexto de ese idioma de todos modos, si trae algunas ventajas debido a la mecánica de ese idioma. Por ejemplo, si desea almacenarlo en una base de datos, probablemente sea sensato convertirlo en propiedad si permite el manejo automático mediante el marco ORM. O tal vez es un lenguaje en el que no hay distinción entre una propiedad y una función miembro sin parámetros (no conozco dicho lenguaje, pero estoy seguro de que hay algunos). Luego, corresponde al diseñador de API y al documentador distinguir entre funciones y propiedades.


Editar: para C # específicamente, miraría las clases existentes, especialmente las de la Biblioteca estándar.

  • Hay BigIntegery Complexestructuras. Pero son estructuras, y solo tienen funciones estáticas, y todas las propiedades son de diferente tipo. Así que no hay mucha ayuda de diseño para tu Headingclase.
  • Math.NET Numerics tiene Vectorclase , que tiene muy pocas propiedades, y parece estar bastante cerca de la tuya Heading. Si sigues esto, entonces tendrías una función para recíproco, no una propiedad.

Es posible que desee ver las bibliotecas realmente utilizadas por su código o las clases similares existentes. Intenta mantenerte constante, eso suele ser lo mejor, si una forma no es claramente correcta y otra equivocada.

Hyde
fuente
-1

Muy interesante pregunta de hecho. No veo ninguna violación de SOLID + DRY + KISS en lo que estás proponiendo, pero, aun así, huele mal.

Un método que devuelve una instancia de la clase se llama constructor , ¿verdad? entonces estás creando una nueva instancia a partir de un método no constructor. No es el fin del mundo, pero como cliente, no esperaría eso. Esto generalmente se hace en circunstancias específicas: una fábrica o, a veces, un método estático de la misma clase (útil para singletons y ... ¿para programadores no tan orientados a objetos?)

Además: ¿qué sucede si invocas getReciprocal()el objeto devuelto? obtienes otra instancia de la clase, ¡que podría ser una copia exacta de la primera! ¿Y si invocas getReciprocal()en ESO? ¿Objetos en expansión a alguien?

De nuevo: ¿qué pasa si el invocador necesita el valor recíproco (como escalar, quiero decir)? getReciprocal()->getValue()? Estamos violando la Ley de Deméter por pequeñas ganancias.

Dr. Gianluigi Zane Zanettini
fuente
Con mucho gusto aceptaría contraargumentos del votante, ¡gracias!
Dr. Gianluigi Zane Zanettini
1
No voté en contra, pero sospecho que la razón podría ser el hecho de que realmente no agrega mucho al resto de las respuestas, pero podría estar equivocado.
53777A
2
SOLID y KISS son a menudo opuestos entre sí.
cuál es el