Digamos, por ejemplo, que tiene una aplicación con una clase ampliamente compartida llamada User
. Esta clase expone toda la información sobre el usuario, su ID, nombre, niveles de acceso a cada módulo, zona horaria, etc.
Obviamente, los datos de usuario están ampliamente referenciados en todo el sistema, pero por cualquier razón, el sistema está configurado de modo que en lugar de pasar este objeto de usuario a clases que dependen de él, solo estamos pasando propiedades individuales de él.
Una clase que requiere la identificación del usuario, simplemente requerirá el GUID userId
como parámetro, a veces también podríamos necesitar el nombre de usuario, por lo que se pasa como un parámetro separado. En algunos casos, esto se pasa a métodos individuales, por lo que los valores no se mantienen a nivel de clase.
Cada vez que necesito acceso a una información diferente de la clase Usuario, tengo que hacer cambios agregando parámetros y cuando agregar una nueva sobrecarga no es apropiado, también tengo que cambiar cada referencia al método o al constructor de la clase.
El usuario es solo un ejemplo. Esto se practica ampliamente en nuestro código.
¿Estoy en lo cierto al pensar que esto es una violación del principio Abierto / Cerrado? ¿No solo el acto de cambiar las clases existentes, sino establecerlas en primer lugar para que sea muy probable que se requieran cambios generalizados en el futuro?
Si acabamos de pasar el User
objeto, podría hacer un pequeño cambio en la clase con la que estoy trabajando. Si tengo que agregar un parámetro, podría tener que hacer docenas de cambios en las referencias a la clase.
¿Hay otros principios quebrantados por esta práctica? ¿Inversión de dependencia quizás? Aunque no estamos haciendo referencia a una abstracción, solo hay un tipo de usuario, por lo que no hay ninguna necesidad real de tener una interfaz de usuario.
¿Se están violando otros principios no SÓLIDOS, como los principios básicos de programación defensiva?
Mi constructor debería verse así:
MyConstructor(GUID userid, String username)
O esto:
MyConstructor(User theUser)
Publicado:
Se ha sugerido que la pregunta se responda en "¿Id. De paso u objeto?". Esto no responde a la pregunta de cómo la decisión de ir de cualquier manera afecta un intento de seguir los principios SÓLIDOS, que es el núcleo de esta pregunta.
I
enSOLID
?MyConstructor
Básicamente dice ahora "Necesito unaGuid
y unastring
". Entonces, ¿por qué no tener una interfaz que proporcione ayGuid
astring
, dejarUser
que implemente esa interfaz yMyConstructor
depender de una instancia que implemente esa interfaz? Y si las necesidades deMyConstructor
cambio, cambiar la interfaz. - Me ayudó mucho pensar en interfaces para "pertenecer" al consumidor en lugar del proveedor . Así que piense "como consumidor , necesito algo que haga esto y aquello" en lugar de "como proveedor puedo hacer esto y aquello".Respuestas:
No hay absolutamente nada de malo en pasar un
User
objeto completo como parámetro. De hecho, podría ayudar a aclarar su código y hacer que sea más obvio para los programadores qué toma un método si la firma del método requiere aUser
.Pasar tipos de datos simples es bueno, hasta que significan algo diferente de lo que son. Considere este ejemplo:
Y un ejemplo de uso:
¿Puedes detectar el defecto? El compilador no puede. El "ID de usuario" que se pasa es solo un número entero. Nombramos la variable
user
pero inicializamos su valor delblogPostRepository
objeto, que presumiblemente devuelveBlogPost
objetos, noUser
objetos; sin embargo, el código se compila y termina con un error de tiempo de ejecución inestable.Ahora considere este ejemplo alterado:
Quizás el
Bar
método solo usa el "Id. De usuario" pero la firma del método requiere unUser
objeto. Ahora volvamos al mismo ejemplo de uso que antes, pero modifíquelo para pasar todo el "usuario" en:Ahora tenemos un error de compilación. El
blogPostRepository.Find
método devuelve unBlogPost
objeto, que inteligentemente llamamos "usuario". Luego pasamos este "usuario" alBar
método y obtenemos rápidamente un error del compilador, porque no podemos pasarBlogPost
a un método que acepte unUser
.El sistema de tipos del lenguaje se está aprovechando para escribir el código correcto más rápido e identificar defectos en tiempo de compilación, en lugar de tiempo de ejecución.
Realmente, tener que refactorizar mucho código porque los cambios en la información del usuario son simplemente un síntoma de otros problemas. Al pasar un
User
objeto completo , obtiene los beneficios anteriores, además de los beneficios de no tener que refactorizar todas las firmas de métodos que aceptan información del usuario cuandoUser
cambia algo sobre la clase.fuente
No, no es una violación de ese principio. Ese principio tiene que ver con no cambiar las
User
formas que afectan otras partes del código que lo usan. Sus cambiosUser
podrían ser una violación, pero no están relacionados.No. Lo que describe, solo inyectando las partes requeridas de un objeto de usuario en cada método, es lo contrario: es pura inversión de dependencia.
No. Este enfoque es una forma perfectamente válida de codificación. No está violando tales principios.
Pero la inversión de dependencia es solo un principio; No es una ley inquebrantable. Y la DI pura puede agregar complejidad al sistema. Si encuentra que solo inyectar los valores de usuario necesarios en los métodos, en lugar de pasar todo el objeto de usuario al método o al constructor, crea problemas, entonces no lo haga de esa manera. Se trata de lograr un equilibrio entre los principios y el pragmatismo.
Para abordar su comentario:
Parte del problema aquí es que claramente no le gusta este enfoque, según el comentario "innecesariamente [pasar] ...". Y eso es justo; No hay una respuesta correcta aquí. Si lo encuentra pesado, no lo haga de esa manera.
Sin embargo, con respecto al principio abierto / cerrado, si sigue eso estrictamente, entonces "... cambie todas las referencias a los cinco métodos existentes ..." es una indicación de que esos métodos fueron modificados, cuando deberían ser cerrado a modificaciones. Sin embargo, en realidad, el principio de abrir / cerrar tiene sentido para las API públicas, pero no tiene mucho sentido para el funcionamiento interno de una aplicación.
Pero luego te paseas por el territorio YAGNI y aún sería ortogonal al principio. Si tiene un método
Foo
que toma un nombre de usuario y luego deseaFoo
tomar una fecha de nacimiento, siguiendo el principio, agregue un nuevo método;Foo
permanece sin cambios. Una vez más, es una buena práctica para las API públicas, pero no tiene sentido para el código interno.Como se mencionó anteriormente, se trata de equilibrio y sentido común para cualquier situación dada. Si esos parámetros a menudo cambian, entonces sí, úselo
User
directamente. Te salvará de los cambios a gran escala que describas. Pero si a menudo no cambian, entonces pasar solo lo que se necesita también es un buen enfoque.fuente
User
instancia y luego consulta ese objeto para obtener un parámetro, entonces solo está invirtiendo parcialmente las dependencias; Todavía hay algunas preguntas en curso. La verdadera inversión de dependencia es 100% "decir, no preguntar". Pero tiene un precio complejo.Sí, cambiar una función existente es una violación del Principio Abierto / Cerrado. Estás modificando algo que debería estar cerrado a modificaciones debido a cambios en los requisitos. Un mejor diseño (para no cambiar cuando cambian los requisitos) sería pasar al Usuario a cosas que deberían funcionar en los usuarios.
Pero que podría ir en contra de la interfaz de Segregación principio, ya que podría estar pasando a lo largo de manera más información que las necesidades de función para hacer su trabajo.
Entonces, como con la mayoría de las cosas, depende .
Usando solo un nombre de usuario, la función sea más flexible, trabajando con nombres de usuario independientemente de su procedencia y sin la necesidad de crear un objeto de Usuario que funcione completamente. Proporciona resistencia al cambio si cree que la fuente de datos cambiará.
El uso de todo el usuario lo hace más claro sobre el uso y hace un contrato más firme con sus llamantes. Proporciona resistencia al cambio si cree que se necesitará más usuario.
fuente
User.find()
. De hecho, ni siquiera debería haber unUser.find
. Encontrar un usuario nunca debe ser responsabilidad deUser
.User
a la función. Quizás eso tenga sentido. Pero tal vez la función solo debería preocuparse por el nombre del usuario, y transmitir cosas como la fecha de unión del usuario o la dirección es incorrecta.User
clase no debería tener. Puede haber unaUserRepository
o similar que se ocupe de tales cosas.Este diseño sigue el patrón de objeto de parámetro . Resuelve problemas que surgen de tener muchos parámetros en la firma del método.
No. La aplicación de este patrón habilita el principio de Abrir / cerrar (OCP). Por ejemplo,
User
se pueden proporcionar clases derivadas de como parámetro que inducen un comportamiento diferente en la clase consumidora.Se puede suceder. Permítanme explicar sobre la base de los principios SÓLIDOS.
El principio de responsabilidad única (SRP) se puede violar si tiene el diseño que usted ha explicado:
El problema es con toda la información . Si la
User
clase tiene muchas propiedades, se convierte en un gran Objeto de transferencia de datos que transporta información no relacionada desde la perspectiva de las clases consumidoras. Ejemplo: desde la perspectiva de una clase consumidora,UserAuthentication
las propiedadesUser.Id
yUser.Name
son relevantes, pero no lo sonUser.Timezone
.El principio de segregación de interfaz (ISP) también se viola con un razonamiento similar, pero agrega otra perspectiva. Ejemplo: supongamos que una clase consumidora
UserManagement
requiere que la propiedadUser.Name
se dividaUser.LastName
yUser.FirstName
la claseUserAuthentication
también se debe modificar para esto.Afortunadamente, el ISP también le brinda una posible solución al problema: por lo general, estos Objetos de parámetro u Objetos de transporte de datos comienzan con poco y crecen con el tiempo. Si esto se vuelve difícil de manejar, considere el siguiente enfoque: Introducir interfaces adaptadas a las necesidades de las clases consumidoras. Ejemplo: Introducir interfaces y dejar que la
User
clase se derive de ella:Cada interfaz debe exponer un subconjunto de propiedades relacionadas de la
User
clase necesarias para que una clase consumidora complete su operación. Busque grupos de propiedades. Intenta reutilizar las interfaces. En el caso de la clase consumidora,UserAuthentication
use enIUserAuthenticationInfo
lugar deUser
. Luego, si es posible, divida laUser
clase en varias clases concretas utilizando las interfaces como "plantilla".fuente
Cuando me enfrenté a este problema en mi propio código, llegué a la conclusión de que las clases / objetos modelo básicos son la respuesta.
Un ejemplo común sería el patrón de repositorio. A menudo, cuando se consulta una base de datos a través de repositorios, muchos de los métodos en el repositorio toman muchos de los mismos parámetros.
Mis reglas generales para los repositorios son:
Cuando más de un método toma los mismos 2 o más parámetros, los parámetros deben agruparse como un objeto modelo.
Cuando un método toma más de 2 parámetros, los parámetros deben agruparse como un objeto modelo.
Los modelos pueden heredar de una base común, pero solo cuando realmente tiene sentido (generalmente es mejor refactorizar más tarde que comenzar con la herencia en mente).
Los problemas con el uso de modelos de otras capas / áreas no se hacen evidentes hasta que el proyecto comienza a volverse un poco complejo. Es solo entonces cuando encuentra que menos código crea más trabajo o más complicaciones.
Y sí, está totalmente bien tener 2 modelos diferentes con propiedades idénticas que sirven para diferentes capas / propósitos (es decir, ViewModels vs POCO).
fuente
Vamos a ver los aspectos individuales de SOLID:
Una cosa que tiende a confundir los instintos de diseño es que la clase es esencialmente para objetos globales y esencialmente de solo lectura. En tal situación, violar las abstracciones no duele mucho: solo leer datos que no se modifican crea un acoplamiento bastante débil; solo cuando se convierte en una enorme pila, el dolor se vuelve notable.
Para restablecer los instintos de diseño, simplemente asuma que el objeto no es muy global. ¿Qué contexto necesitaría una función si el
User
objeto pudiera mutar en cualquier momento? ¿Qué componentes del objeto probablemente mutarían juntos? Estos pueden dividirseUser
, ya sea como un subobjeto referenciado o como una interfaz que expone solo una "porción" de campos relacionados, no es tan importante.Otro principio:
User
observe las funciones que usan partes de y vea qué campos (atributos) tienden a ir juntos. Esa es una buena lista preliminar de subobjetos: definitivamente debes pensar si realmente pertenecen juntos.Es mucho trabajo, y es algo difícil de hacer, y su código se volverá un poco menos flexible porque será un poco más difícil identificar el subobjeto (subinterfaz) que debe pasar a una función, particularmente si los subobjetos tienen superposiciones.
La división
User
en realidad se volverá fea si los subobjetos se superponen, entonces las personas se confundirán acerca de cuál elegir si todos los campos requeridos son de la superposición. Si se divide jerárquicamente (por ejemplo, tieneUserMarketSegment
cuál, entre otras cosas, tieneUserLocation
), las personas no estarán seguras en qué nivel se encuentra la función que están escribiendo: ¿se trata de datos de usuario en elLocation
nivel o en elMarketSegment
nivel? No ayuda exactamente que esto pueda cambiar con el tiempo, es decir, volver a cambiar las firmas de funciones, a veces en toda una cadena de llamadas.En otras palabras: a menos que realmente conozca su dominio y tenga una idea bastante clara de qué módulo se ocupa de qué aspectos
User
, realmente no vale la pena mejorar la estructura del programa.fuente
Esta es una pregunta realmente interesante. Depende
Si cree que su método puede cambiar internamente en el futuro para requerir diferentes parámetros del objeto Usuario, ciertamente debe pasarlo todo. La ventaja es que el código externo al método está protegido de los cambios dentro del método en términos de los parámetros que está utilizando, lo que, como usted dice, causaría una cascada de cambios externos. Entonces, pasar todo el usuario aumenta la encapsulación.
Si está seguro de que nunca necesitará usar nada más que decir el correo electrónico del Usuario, debe pasarlo. La ventaja de esto es que puede usar el método en una gama más amplia de contextos: puede usarlo, por ejemplo. con el correo electrónico de una empresa o con un correo electrónico que alguien acaba de escribir. Esto aumenta la flexibilidad.
Esto es parte de una gama más amplia de preguntas sobre cómo construir clases para tener un alcance amplio o estrecho, incluyendo si se inyectan dependencias o no y si se tienen objetos disponibles globalmente. Hay una desafortunada tendencia en este momento a pensar que un alcance más estrecho siempre es bueno. Sin embargo, siempre hay una compensación entre encapsulación y flexibilidad como en este caso.
fuente
Creo que es mejor pasar la menor cantidad posible de parámetros y la cantidad necesaria. Esto facilita las pruebas y no requiere el encajonamiento de objetos completos.
En su ejemplo, si va a usar solo la identificación de usuario o el nombre de usuario, esto es todo lo que debe pasar. Si este patrón se repite varias veces y el objeto de usuario real es mucho más grande, entonces mi consejo es crear una interfaz más pequeña para eso. Podría ser
o
Esto hace que las pruebas con burla sean mucho más fáciles y usted sabe instantáneamente qué valores se usan realmente. De lo contrario, a menudo necesita inicializar objetos complejos con muchas otras dependencias, aunque al final solo necesita una o dos propiedades.
fuente
Aquí hay algo que he encontrado de vez en cuando:
User
(oProduct
lo que sea) que tiene muchas propiedades, aunque el método solo usa algunas de ellas.User
objeto completamente poblado . Crea una instancia e inicializa solo las propiedades que el método realmente necesita.User
argumento, tiene que buscar llamadas a ese método para saber de dóndeUser
proviene para que sepa qué propiedades están pobladas. ¿Es un usuario "real" con una dirección de correo electrónico, o simplemente se creó para pasar una identificación de usuario y algunos permisos?Si crea una
User
y solo completa algunas propiedades porque esas son las que necesita el método, entonces la persona que llama realmente sabe más sobre el funcionamiento interno del método de lo que debería.Peor aún, cuando se tiene una instancia de
User
, usted tiene que saber de dónde vino para que sepa qué propiedades están pobladas. No quieres tener que saber eso.Con el tiempo, cuando los desarrolladores ven
User
como un contenedor para argumentos de métodos, podrían comenzar a agregarle propiedades para escenarios de un solo uso. Ahora se está poniendo feo, porque la clase se está abarrotando con propiedades que casi siempre serán nulas o predeterminadas.Tal corrupción no es inevitable, pero ocurre una y otra vez cuando pasamos un objeto solo porque necesitamos acceso a algunas de sus propiedades. La zona de peligro es la primera vez que ve a alguien creando una instancia
User
y rellenando algunas propiedades para que pueda pasarla a un método. Pon tu pie sobre él porque es un camino oscuro.Siempre que sea posible, establezca el ejemplo correcto para el próximo desarrollador pasando solo lo que necesita pasar.
fuente