Tengo un pequeño debate con mi amigo sobre si estas dos prácticas son simplemente dos caras de la misma moneda, o si una es realmente mejor.
Tenemos una función que toma un parámetro, completa un miembro y luego lo devuelve:
Item predictPrice(Item item)
Creo que, dado que funciona en el mismo objeto que se pasa, no tiene sentido devolver el artículo. De hecho, en todo caso, desde la perspectiva de la persona que llama, confunde las cosas, ya que podría esperar que devuelva un nuevo elemento que no lo hace.
Afirma que no hay diferencia, y que ni siquiera importaría si creara un nuevo Artículo y lo devolviera. Estoy totalmente en desacuerdo por las siguientes razones:
Si tiene múltiples referencias al elemento que pasa (o punteros o lo que sea), asignar un nuevo objeto y devolverlo es de importancia material ya que esas referencias serán incorrectas.
En lenguajes no gestionados por memoria, la función que asigna una nueva instancia reclama la propiedad de la memoria y, por lo tanto, tendríamos que implementar un método de limpieza que se llama en algún momento.
Asignar en el montón es potencialmente costoso y, por lo tanto, es importante si la función llamada lo hace.
Por lo tanto, creo que es muy importante poder ver a través de una firma de métodos si modifica un objeto o asigna uno nuevo. Como resultado, creo que como la función simplemente modifica el objeto pasado, la firma debería ser:
void predictPrice(Item item)
En cada base de código (ciertamente, bases de código C y C ++, no en Java, que es el lenguaje en el que estamos trabajando) con las que he trabajado, el estilo anterior se ha cumplido esencialmente y se ha mantenido por programadores considerablemente más experimentados. Afirma que, dado que el tamaño de mi muestra de bases de código y colegas es pequeño de todas las posibles bases de código y colegas, y por lo tanto, mi experiencia no es un verdadero indicador sobre si uno es superior.
Entonces, ¿alguna idea?
fuente
Item
esclass Item ...
y notypedef ...& Item
, el localitem
ya es una copiaRespuestas:
Esto realmente es una cuestión de opinión, pero para lo que vale, me resulta engañoso devolver el artículo modificado si el artículo se modifica en su lugar. Por separado, si
predictPrice
va a modificar el elemento, debe tener un nombre que indique que va a hacer eso (comosetPredictPrice
o algo así).Preferiría (en orden)
Ese
predictPrice
fue un método deItem
(módulo una buena razón para que no lo sea, que bien podría haber en su diseño general) queDevuelto el precio previsto, o
Tenía un nombre como en su
setPredictedPrice
lugarEso
predictPrice
no modificóItem
, pero devolvió el precio previstoEse
predictPrice
era unvoid
método llamadosetPredictedPrice
Eso
predictPrice
regresóthis
(para el método de encadenamiento a otros métodos en cualquier instancia de la que sea parte) (y fue llamadosetPredictedPrice
)fuente
a = doSomethingWith(b)
modifiquéb
y lo devolví con las modificaciones, que explotaron mi código más tarde y pensé que sabía lo queb
había pasado. en eso. :-)Dentro de un paradigma de diseño orientado a objetos, las cosas no deberían estar modificando objetos fuera del objeto mismo. Cualquier cambio en el estado de un objeto debe hacerse a través de métodos en el objeto.
Por lo tanto,
void predictPrice(Item item)
como función miembro de alguna otra clase está mal . Podría haber sido aceptable en los días de C, pero para Java y C ++, las modificaciones de un objeto implican un acoplamiento más profundo con el objeto que probablemente conduciría a otros problemas de diseño en el futuro (cuando refactoriza la clase y cambia sus campos, ahora que 'predicPrice' debe cambiarse en algún otro archivo.Devolver un nuevo objeto, no tiene efectos secundarios asociados, el parámetro pasado no cambia. Usted (el método predictPrice) no sabe dónde más se usa ese parámetro. ¿Es
Item
una clave para un hash en alguna parte? ¿Cambiaste su código hash al hacer esto? ¿Alguien más lo está esperando esperando que no cambie?Estos problemas de diseño son los que sugieren fuertemente que no debería estar modificando objetos (yo argumentaría por la inmutabilidad en muchos casos), y si lo hace, los cambios en el estado deberían estar contenidos y controlados por el propio objeto en lugar de algo más fuera de la clase.
Veamos qué sucede si uno juega con los campos de algo en un hash. Tomemos un código:
ideona
Y admito que este no es el mejor código (acceso directo a los campos), pero sirve para demostrar un problema con los datos mutables que se utilizan como clave para un HashMap, o en este caso, simplemente se ponen en un HashSet.
La salida de este código es:
Lo que sucedió es que el hashCode utilizado cuando se insertó es donde está el objeto en el hash. Cambiar los valores utilizados para calcular el hashCode no vuelve a calcular el hash en sí. Esto es un peligro de poner cualquier objeto mutable como clave para un hash.
Por lo tanto, volviendo al aspecto original de la pregunta, el método que se llama no "sabe" cómo se está utilizando el objeto que está obteniendo como parámetro. Proporcionar "permite mutar el objeto" como la única forma de hacer esto significa que hay una serie de errores sutiles que pueden colarse. Como perder valores en un hash ... a menos que agregue más y el hash se vuelva a agregar, confíe en mí , eso es un error desagradable para cazar (perdí el valor hasta que agregué 20 elementos más al hashMap y luego, de repente, vuelve a aparecer).
Relacionado: sobrescribir y devolver el valor del argumento utilizado como condicional de una instrucción if, dentro de la misma instrucción if
fuente
predictPrice
que no deba jugar directamente conItem
los miembros de, pero ¿qué hay de malo en llamar a los métodosItem
para hacerlo? Si ese es el único objeto que se está modificando, tal vez pueda argumentar que debería ser un método del objeto que se está modificando, pero otras veces se están modificando varios objetos (que definitivamente no deberían ser de la misma clase), especialmente en métodos más bien de alto nivel.Siempre sigo la práctica de no mutar el objeto de otra persona. Es decir, solo objetos mutantes propiedad de la clase / estructura / lo que está dentro.
Dada la siguiente clase de datos:
Esto esta bien:
Y esto es muy claro:
Pero esto es demasiado fangoso y misterioso para mis gustos:
fuente
La sintaxis es diferente entre los diferentes idiomas, y no tiene sentido mostrar un código que no es específico de un idioma porque significa cosas completamente diferentes en diferentes idiomas.
En Java,
Item
es un tipo de referencia: es el tipo de punteros a objetos que son instancias deItem
. En C ++, este tipo se escribe comoItem *
(la sintaxis de la misma cosa es diferente entre los lenguajes). EntoncesItem predictPrice(Item item)
en Java es equivalente aItem *predictPrice(Item *item)
en C ++. En ambos casos, es una función (o método) que toma un puntero a un objeto y devuelve un puntero a un objeto.En C ++,
Item
(sin otra cosa) es un tipo de objeto (suponiendo queItem
es el nombre de una clase). Un valor de este tipo "es" un objeto yItem predictPrice(Item item)
declara una función que toma un objeto y devuelve un objeto. Como&
no se usa, se pasa y se devuelve por valor. Eso significa que el objeto se copia cuando se pasa y se copia cuando se devuelve. No hay equivalente en Java porque Java no tiene tipos de objeto.Entonces cual es? Muchas de las cosas que hace en la pregunta (por ejemplo, "Creo que, dado que funciona en el mismo objeto que se pasa ...") depende de comprender exactamente lo que se pasa y se devuelve aquí.
fuente
La convención a la que he visto adherirse a las bases de código es preferir un estilo que sea claro a partir de la sintaxis. Si la función cambia de valor, prefiera pasar por puntero
Este estilo da como resultado:
Al llamarlo ves:
predicPrice (& my_item);
y sabes que será modificado por tu adherencia al estilo.
fuente
Si bien no tengo una gran preferencia, votaría que devolver el objeto conduce a una mejor flexibilidad para una API.
Tenga en cuenta que solo tiene sentido mientras el método tiene libertad para devolver otro objeto. Si tu javadoc dice
@returns the same value of parameter a
que es inútil. Pero si su javadoc lo dice@return an instance that holds the same data than parameter a
, devolver la misma instancia u otra es un detalle de implementación.Por ejemplo, en mi proyecto actual (Java EE) tengo una capa empresarial; para almacenar en DB (y asignar una identificación automática) un empleado, por ejemplo, un empleado que tengo algo como
Por supuesto, no hay diferencia con esta implementación porque JPA devuelve el mismo objeto después de asignar el Id. Ahora, si me cambié a JDBC
Ahora en ????? Tengo tres opciones
Defina un setter para Id, y devolveré el mismo objeto. Feo, porque
setId
estará disponible en todas partes.Haga un trabajo pesado de reflexión y configure la identificación en el
Employee
objeto. Sucio, pero al menos se limita alcreateEmployee
registro.Proporcione un constructor que copie el original.
Employee
y acepte también elid
conjunto.Recupere el objeto de la base de datos (tal vez sea necesario si algunos valores de campo se calculan en la base de datos, o si desea llenar una relación real).
Ahora, para 1. y 2. puede devolver la misma instancia, pero para 3. y 4. devolverá nuevas instancias. Si desde el principio estableciste en tu API que el valor "real" será el que devuelve el método, tienes más libertad.
El único argumento real en el que puedo pensar es que, usando implementaciones 1. o 2., las personas que usan su API pueden pasar por alto la asignación del resultado y su código se rompería si cambia a implementaciones 3. o 4.).
De todos modos, como puede ver, mi preferencia se basa en otras preferencias personales (como prohibir un setter para Ids), por lo que tal vez no se aplique a todos.
fuente
Al codificar en Java, se debe intentar hacer que el uso de cada objeto de tipo mutable coincida con uno de dos patrones:
Exactamente una entidad se considera el "propietario" del objeto mutable y considera el estado de ese objeto como parte del propio. Otras entidades pueden tener referencias, pero deben considerar la referencia como la identificación de un objeto que es propiedad de otra persona.
A pesar de que el objeto es mutable, nadie que tenga una referencia a él puede mutarlo, lo que hace que la instancia sea efectivamente inmutable (tenga en cuenta que debido a las peculiaridades en el modelo de memoria de Java, no hay una buena manera de hacer que un objeto efectivamente inmutable tenga hilo- semántica inmutable segura sin agregar un nivel adicional de indirección a cada acceso). Cualquier entidad que encapsule el estado utilizando una referencia a dicho objeto y quiera cambiar el estado encapsulado debe crear un nuevo objeto que contenga el estado adecuado.
No me gusta que los métodos muten el objeto subyacente y devuelvan una referencia, ya que dan la apariencia de ajustarse al segundo patrón anterior. Hay algunos casos en los que el enfoque puede ser útil, pero el código que va a mutar objetos debería parecer que lo hará; el código similar se
myThing = myThing.xyz(q);
parece mucho menos a que muta el objeto identificado de lomyThing
que lo haría simplementemyThing.xyz(q);
.fuente