En el pasado, normalmente he realizado la mayor parte de mi manipulación de un objeto dentro del método principal que se está creando / actualizando, pero últimamente me he encontrado con un enfoque diferente, y tengo curiosidad si es una mala práctica.
Aquí hay un ejemplo. Digamos que tengo un repositorio que acepta una User
entidad, pero antes de insertar la entidad, llamamos a algunos métodos para asegurarnos de que todos sus campos estén configurados para lo que queremos. Ahora, en lugar de llamar a métodos y establecer los valores de campo desde el método Insertar, llamo a una serie de métodos de preparación que dan forma al objeto antes de su inserción.
Método antiguo
public void InsertUser(User user) {
user.Username = GenerateUsername(user);
user.Password = GeneratePassword(user);
context.Users.Add(user);
}
Nuevos métodos
public void InsertUser(User user) {
SetUsername(user);
SetPassword(user);
context.Users.Add(user);
}
private void SetUsername(User user) {
var username = "random business logic";
user.Username = username;
}
private void SetPassword(User user) {
var password = "more business logic";
user.Password = password;
}
Básicamente, ¿la práctica de establecer el valor de una propiedad desde otro método es una mala práctica?
fuente
user
se pasa por referencia, el código podría dar un tirón hacia fuera de las manos de la persona que llama y reemplazarlo con sólo decir, por ejemplo,user = null;
.Respuestas:
El problema aquí es que a
User
puede contener dos cosas diferentes:Una entidad de usuario completa, que puede pasar a su almacén de datos.
El conjunto de elementos de datos requeridos por la persona que llama para comenzar el proceso de creación de una entidad de usuario. El sistema debe agregar un nombre de usuario y una contraseña antes de que sea realmente un usuario válido, como se indica en el punto 1 anterior.
Esto comprende un matiz no documentado para su modelo de objetos que no se expresa en su sistema de tipos en absoluto. Solo necesita "saberlo" como desarrollador. Eso no es genial, y conduce a patrones de código extraños como el que estás encontrando.
Te sugiero que necesites dos entidades, por ejemplo, una
User
clase y unaEnrollRequest
clase. Este último puede contener todo lo que necesita saber para crear un usuario. Se vería como su clase de usuario, pero sin el nombre de usuario y la contraseña. Entonces podrías hacer esto:La persona que llama comienza con solo información de inscripción y recupera al usuario completado después de que se inserta. De esta manera, evitas mutar cualquier clase y también tienes una distinción de tipo seguro entre un usuario que está insertado y uno que no lo está.
fuente
InsertUser
Es probable que un método con un nombre como tenga el efecto secundario de la inserción. No estoy seguro de lo que significa "repulsivo" en este contexto. En cuanto al uso de un "usuario" que no se ha agregado, parece que has perdido el punto. Si no se ha agregado, no es un usuario, solo una solicitud para crear un usuario. Eres libre de trabajar con la solicitud, por supuesto.Los efectos secundarios están bien siempre que no sean inesperados. Por lo tanto, no hay nada malo en general cuando un repositorio tiene un método que acepta a un usuario y cambia su estado interno. Pero en mi humilde opinión, un nombre de método como
InsertUser
no comunica claramente esto , y eso es lo que lo hace propenso a errores. Cuando use su repositorio, esperaría una llamada comopara cambiar el estado interno de los repositorios, no el estado del objeto del usuario. Este problema existe en ambas implementaciones, ¿cómo
InsertUser
es que esto es completamente irrelevante?Para resolver esto, podrías
inicialización separada de la inserción (por lo que la persona que llama
InsertUser
necesita proporcionar unUser
objeto completamente inicializado , oconstruir la inicialización en el proceso de construcción del objeto de usuario (como lo sugieren algunas de las otras respuestas), o
simplemente trate de encontrar un mejor nombre para el método que exprese más claramente lo que hace.
Así que elija un nombre de método, como
PrepareAndInsertUser
,OrchestrateUserInsertion
,InsertUserWithNewNameAndPassword
o lo que usted prefiere hacer el efecto secundario más clara.Por supuesto, un nombre de método tan largo indica que el método puede estar haciendo "demasiado" (violación de SRP), pero a veces no quiere o no puede solucionarlo fácilmente, y esta es la solución menos intrusiva y pragmática.
fuente
Estoy mirando las dos opciones que ha elegido, y tengo que decir que, con mucho, prefiero el método anterior en lugar del nuevo método propuesto. Hay algunas razones para ello, aunque fundamentalmente hacen lo mismo.
En cualquier caso, está configurando el
user.UserName
yuser.Password
. Tengo reservas sobre el elemento de contraseña, pero esas reservas no están relacionadas con el tema en cuestión.Implicaciones de modificar objetos de referencia
Método antiguo versus método nuevo
El viejo método hacía las cosas más fáciles de probar:
GenerateUserName()
Es independientemente comprobable. Puede escribir pruebas contra ese método y asegurarse de que los nombres se generen correctamenteGenerateUserName(User user)
y mantener esa capacidad de pruebaEl nuevo método oculta las mutaciones:
User
objeto está cambiando hasta que vas a 2 capas de profundidadUser
objeto son más sorprendentes en ese caso.SetUserName()
hace más que establecer un nombre de usuario. Eso no es verdad en la publicidad, lo que dificulta que los nuevos desarrolladores descubran cómo funcionan las cosas en su aplicaciónfuente
Estoy totalmente de acuerdo con la respuesta de John Wu. Su sugerencia es buena. Pero extraña un poco tu pregunta directa.
No inherentemente
No puede llevar esto demasiado lejos, ya que se encontrará con un comportamiento inesperado. Por ejemplo
PrintName(myPerson)
, no debería estar cambiando el objeto persona, ya que el método implica que solo le interesa leer los valores existentes. Pero ese es un argumento diferente al de su caso, yaSetUsername(user)
que implica fuertemente que va a establecer valores.Este es en realidad un enfoque que uso a menudo para las pruebas de unidad / integración, donde creo un método específicamente para alterar un objeto con el fin de establecer sus valores en una situación particular que quiero probar.
Por ejemplo:
Explícitamente espero que el
ArrrangeContractDeletedStatus
método cambie el estado delmyContract
objeto.El principal beneficio es que el método me permite probar la eliminación del contrato con diferentes contratos iniciales; por ejemplo, un contrato que tiene un historial de estado largo, o uno que no tiene historial de estado previo, un contrato que tiene un historial de estado intencionalmente erróneo, un contrato que mi usuario de prueba no puede eliminar.
Si me hubiera fusionado
CreateEmptyContract
yArrrangeContractDeletedStatus
en un solo método; Tendría que crear múltiples variantes de este método para cada contrato diferente que quisiera probar en un estado eliminado.Y aunque podría hacer algo como:
Esto es redundante (ya que estoy cambiando el objeto de todos modos), o ahora me estoy obligando a hacer un clon profundo del
myContract
objeto; lo cual es excesivamente difícil si quieres cubrir cada caso (imagina si quiero varios niveles de propiedades de navegación. ¿Necesito clonarlos todos? ¿Solo la entidad de nivel superior? ... Tantas preguntas, tantas expectativas implícitas )Cambiar el objeto es la forma más fácil de obtener lo que quiero sin tener que hacer más trabajo solo para evitar no cambiar el objeto por principio.
Entonces, la respuesta directa a su pregunta es que no es inherentemente una mala práctica, siempre y cuando no ofusque que el método pueda cambiar el objeto pasado. Para su situación actual, eso queda muy claro a través del nombre del método.
fuente
Encuentro lo viejo y lo nuevo problemático.
Vieja forma:
1)
InsertUser(User user)
Al leer el nombre de este método que aparece en mi IDE, lo primero que me viene a la mente es
El nombre del método debería leer
AddUserToContext
. Al menos, esto es lo que hace el método al final.2)
InsertUser(User user)
Esto viola claramente el principio de la menor sorpresa. Digamos que hice mi trabajo hasta ahora y creé una nueva instancia de a
User
y le di unaname
ypassword
puse una , me sorprendería:a) esto no solo inserta un usuario en algo
b) también subvierte mi intención de insertar al usuario tal como está; el nombre y la contraseña fueron anulados.
c) ¿indica esto que también es una violación del principio de responsabilidad única?
Nueva manera:
1)
InsertUser(User user)
Todavía violación de SRP y principio de la menor sorpresa
2)
SetUsername(User user)
Pregunta
Mejor:
SetRandomName
o algo que refleje la intención.3)
SetPassword(User user)
Pregunta
Mejor:
SetRandomPassword
o algo que refleje la intención.Sugerencia:
Lo que preferiría leer sería algo como:
Con respecto a su pregunta inicial:
No. Es más una cuestión de gustos.
fuente