Estaba tratando de encontrar alternativas al uso de la variable global en algún código heredado. Pero esta pregunta no se trata de las alternativas técnicas, me preocupa principalmente la terminología .
La solución obvia es pasar un parámetro a la función en lugar de usar un global. En esta base de código heredada, eso significaría que tengo que cambiar todas las funciones en la larga cadena de llamadas entre el punto donde finalmente se usará el valor y la función que recibe el parámetro primero.
higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)
donde newParam
anteriormente era una variable global en mi ejemplo, pero podría haber sido un valor previamente codificado en su lugar. El punto es que ahora el valor de newParam se obtiene higherlevel()
y tiene que "viajar" hasta el final level3()
.
Me preguntaba si había un nombre (s) para este tipo de situación / patrón donde necesita agregar un parámetro a muchas funciones que simplemente "pasan" el valor sin modificar.
Con suerte, usar la terminología adecuada me permitirá encontrar más recursos sobre soluciones para rediseñar y describir esta situación a mis colegas.
Respuestas:
Los datos en sí mismos se denominan "datos de vagabundo" . Es un "olor a código", que indica que un código se está comunicando con otro código a distancia, a través de intermediarios.
Refactorizar para eliminar variables globales es difícil, y los datos de vagabundeo son un método para hacerlo, y a menudo la forma más barata. Tiene sus costos.
fuente
No creo que esto, en sí mismo, sea un antipatrón. Creo que el problema es que estás pensando en las funciones como una cadena cuando realmente deberías pensar en cada una como una caja negra independiente ( NOTA : los métodos recursivos son una notable excepción a este consejo).
Por ejemplo, supongamos que necesito calcular el número de días entre dos fechas de calendario para crear una función:
Para hacer esto, creo una nueva función:
Entonces mi primera función se convierte simplemente:
No hay nada antipatrón sobre esto. Los parámetros del método daysBetween se pasan a otro método y nunca se hace referencia de otro modo en el método, pero aún son necesarios para que ese método haga lo que debe hacer.
Lo que recomendaría es mirar cada función y comenzar con un par de preguntas:
Si está buscando una mezcla de código sin un solo propósito incluido en un método, debe comenzar desentrañando eso. Esto puede ser tedioso. Comience con las cosas más fáciles de sacar y pase a un método separado y repita hasta que tenga algo coherente.
Si solo tiene demasiados parámetros, considere la refactorización de Método a Objeto .
fuente
BobDalgleish ya ha notado que este patrón (anti) se llama " datos de vagabundo ".
En mi experiencia, la causa más común de datos de vagabundeo excesivos es tener un montón de variables de estado vinculadas que realmente deberían encapsularse en un objeto o una estructura de datos. A veces, incluso puede ser necesario anidar un montón de objetos para organizar adecuadamente los datos.
Para un ejemplo simple, considere un juego que tiene un personaje de jugador personalizable, con propiedades como
playerName
,playerEyeColor
etc. Por supuesto, el jugador también tiene una posición física en el mapa del juego y varias otras propiedades como, por ejemplo, el nivel de salud actual y máximo, y así sucesivamente.En una primera iteración de un juego de este tipo, podría ser una opción perfectamente razonable convertir todas estas propiedades en variables globales: después de todo, solo hay un jugador, y casi todo en el juego de alguna manera involucra al jugador. Entonces su estado global puede contener variables como:
Pero en algún momento, es posible que necesites cambiar este diseño, tal vez porque deseas agregar un modo multijugador al juego. Como primer intento, puede intentar hacer que todas esas variables sean locales y pasarlas a las funciones que las necesitan. Sin embargo, puede encontrar que una acción particular en su juego podría involucrar una cadena de llamada de función como, por ejemplo:
... y la
interactWithShopkeeper()
función hace que el comerciante dirija al jugador por su nombre, por lo que ahora de repente debe pasarplayerName
como datos de vagabundo a través de todas esas funciones. Y, por supuesto, si el comerciante cree que los jugadores de ojos azules son ingenuos y cobrarán precios más altos por ellos, entonces necesitará pasarplayerEyeColor
por toda la cadena de funciones, y así sucesivamente.La solución adecuada , en este caso, es, por supuesto, definir un objeto jugador que encapsule el nombre, color de ojos, posición, salud y cualquier otra propiedad del personaje jugador. De esa manera, solo necesita pasar ese único objeto a todas las funciones que de alguna manera involucran al jugador.
Además, varias de las funciones anteriores podrían convertirse naturalmente en métodos de ese objeto jugador, lo que automáticamente les daría acceso a las propiedades del jugador. En cierto modo, esto es solo azúcar sintáctico, ya que llamar a un método en un objeto efectivamente pasa la instancia del objeto como un parámetro oculto al método de todos modos, pero hace que el código se vea más claro y más natural si se usa correctamente.
Por supuesto, un juego típico tendría mucho más estado "global" que solo el jugador; por ejemplo, es casi seguro que tengas algún tipo de mapa en el que se desarrolla el juego, y una lista de personajes que no son jugadores que se mueven en el mapa, y tal vez elementos colocados en él, y así sucesivamente. También podría pasar todos esos objetos vagabundos, pero eso volvería a saturar los argumentos de su método.
En cambio, la solución es hacer que los objetos almacenen referencias a cualquier otro objeto con el que tengan relaciones permanentes o temporales. Entonces, por ejemplo, el objeto jugador (y probablemente también cualquier objeto NPC) probablemente debería almacenar una referencia al objeto "mundo del juego", que tendría una referencia al nivel / mapa actual, de modo que un método como
player.moveTo(x, y)
no necesita se le dará explícitamente el mapa como parámetro.De manera similar, si nuestro personaje jugador tuviera, por ejemplo, un perro mascota que los siguiera, naturalmente agruparíamos todas las variables de estado que describen al perro en un solo objeto, y le daríamos al objeto jugador una referencia al perro (para que el jugador pueda , por ejemplo, llame al perro por su nombre) y viceversa (para que el perro sepa dónde está el jugador). Y, por supuesto, probablemente querríamos que el jugador y el perro se conviertan en subclases de un objeto "actor" más genérico, de modo que podamos reutilizar el mismo código para, por ejemplo, mover ambos por el mapa.
PD. Aunque he usado un juego como ejemplo, hay otros tipos de programas en los que también surgen estos problemas. Sin embargo, en mi experiencia, el problema subyacente tiende a ser siempre el mismo: tiene un montón de variables separadas (ya sean locales o globales) que realmente quieren agruparse en uno o más objetos interconectados. Si los "datos de vagabundo" que se entrometen en sus funciones consisten en configuraciones de opciones "globales" o consultas de bases de datos en caché o vectores de estado en una simulación numérica, la solución es invariablemente identificar el contexto natural al que pertenecen los datos y convertirlo en un objeto (o lo que sea el equivalente más cercano en el idioma elegido).
fuente
foo.method(bar, baz)
ymethod(foo, bar, baz)
hay otras razones (incluido el polimorfismo, la encapsulación, la localidad, etc.) para preferir el primero.No conozco un nombre específico para esto, pero creo que vale la pena mencionar que el problema que describe es solo el problema de encontrar el mejor compromiso para el alcance de dicho parámetro:
Como variable global, el alcance es demasiado grande cuando el programa alcanza cierto tamaño
como un parámetro puramente local, el alcance puede ser demasiado pequeño, cuando conduce a muchas listas de parámetros repetitivos en cadenas de llamadas
entonces, como compensación, a menudo puede hacer que dicho parámetro sea una variable miembro en una o más clases, y eso es lo que yo llamaría un diseño de clase adecuado .
fuente
Creo que el patrón que estás describiendo es exactamente una inyección de dependencia . Varios comentaristas han argumentado que este es un patrón , no un antipatrón , y yo tendería a estar de acuerdo.
También estoy de acuerdo con la respuesta de @ JimmyJames, donde afirma que es una buena práctica de programación tratar cada función como un cuadro negro que toma todas sus entradas como parámetros explícitos. Es decir, si está escribiendo una función que hace un sándwich de mantequilla de maní y mermelada, podría escribirlo como
pero sería una mejor práctica aplicar la inyección de dependencia y escribirla así:
Ahora tiene una función que documenta claramente todas sus dependencias en su firma de función, lo cual es excelente para facilitar la lectura. Después de todo, es cierto que para poder
make_sandwich
necesitar acceder a unRefrigerator
; así que la antigua firma de la función era básicamente falsa al no tomar el refrigerador como parte de sus entradas.Como beneficio adicional, si hace bien su jerarquía de clases, evite cortar, y así sucesivamente, ¡incluso puede probar la
make_sandwich
función de la unidad pasando unMockRefrigerator
! (Es posible que deba realizar una prueba unitaria de esta manera porque su entorno de prueba unitaria podría no tener acceso a ningúnPhysicalRefrigerator
s).Entiendo que no todos los usos de la inyección de dependencia requieren conectar un parámetro con un nombre similar a muchos niveles en la pila de llamadas, por lo que no estoy respondiendo exactamente la pregunta que hizo ... pero si está buscando más información sobre este tema, "inyección de dependencia" es definitivamente una palabra clave relevante para usted.
fuente
Refrigerator
en unIngredientSource
, o incluso generalizar la noción de "sandwich" entemplate<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&)
; eso se llama "programación genérica" y es razonablemente poderoso, pero seguramente es mucho más arcano de lo que el OP realmente quiere entrar en este momento. Siéntase libre de abrir una nueva pregunta sobre el nivel adecuado de abstracción para los programas sandwich. ;)make_sandwich()
.Esta es más o menos la definición de libro de texto de acoplamiento , un módulo que tiene una dependencia que afecta profundamente a otro, y que crea un efecto dominó cuando se cambia. Los otros comentarios y respuestas son correctos de que esta es una mejora con respecto a la global, porque el acoplamiento ahora es más explícito y más fácil de ver para el programador, en lugar de subversivo. Eso no significa que no deba repararse. Debería poder refactorizar para quitar o reducir el acoplamiento, aunque si ha estado allí por un tiempo, puede ser doloroso.
fuente
level3()
necesarionewParam
, eso es seguro, pero de alguna manera diferentes partes del código tienen que comunicarse entre sí. No llamaría necesariamente un mal acoplamiento de un parámetro de función si esa función hace uso del parámetro. Creo que el aspecto problemático de la cadena es el acoplamiento adicional introducidolevel1()
ylevel2()
que no tiene ningún usonewParam
excepto para transmitirlo. Buena respuesta, +1 por acoplamiento.Si bien esta respuesta no responde directamente a su pregunta, creo que sería negligente dejarla pasar sin mencionar cómo mejorarla (ya que, como usted dice, puede ser un antipatrón). Espero que usted y otros lectores puedan obtener valor de este comentario adicional sobre cómo evitar los "datos de vagabundo" (como Bob Dalgleish lo llamó tan útilmente para nosotros).
Estoy de acuerdo con las respuestas que sugieren hacer algo más OO para evitar este problema. Sin embargo, otra forma de ayudar a reducir profundamente este paso de argumentos sin simplemente saltar a "¡ simplemente pasar una clase donde solías pasar muchos argumentos! " Es refactorizar para que algunos pasos de tu proceso ocurran en el nivel superior en lugar del nivel inferior uno. Por ejemplo, aquí hay un código anterior :
Tenga en cuenta que esto se vuelve aún peor a medida que se hacen más cosas
ReportStuff
. Es posible que deba pasar la instancia del Reportero que desea usar. Y todo tipo de dependencias que deben transmitirse, función a función anidada.Mi sugerencia es llevar todo eso a un nivel superior, donde el conocimiento de los pasos requiere vidas en un solo método en lugar de extenderse a través de una cadena de llamadas a métodos. Por supuesto, sería más complicado en código real, pero esto te da una idea:
Tenga en cuenta que la gran diferencia aquí es que no tiene que pasar las dependencias a través de una cadena larga. Incluso si se aplana no solo a un nivel, sino a unos pocos niveles de profundidad, si esos niveles también logran algo de "aplanamiento" para que el proceso se vea como una serie de pasos en ese nivel, habrá mejorado.
Si bien esto todavía es de procedimiento y nada se ha convertido en un objeto todavía, es un buen paso para decidir qué tipo de encapsulación puede lograr al convertir algo en una clase. Las llamadas de método profundamente encadenadas en el escenario anterior ocultan los detalles de lo que realmente está sucediendo y pueden hacer que el código sea muy difícil de entender. Si bien puede exagerar esto y terminar haciendo que el código de nivel superior sepa sobre cosas que no debería, o hacer un método que haga demasiadas cosas violando así el principio de responsabilidad única, en general he descubierto que aplanar un poco las cosas ayuda en claridad y en hacer un cambio incremental hacia un mejor código.
Tenga en cuenta que mientras hace todo esto, debe considerar la capacidad de prueba. El método encadenado en realidad llama hacer las pruebas unitarias más difícil , ya que no tiene un buen punto de entrada y punto de salida en el montaje para el sector que desee probar. Tenga en cuenta que con este aplanamiento, ya que sus métodos ya no requieren tantas dependencias, son más fáciles de probar, ¡y no requieren tantos simulacros!
Recientemente intenté agregar pruebas unitarias a una clase (que no escribí) que tomó algo así como 17 dependencias, ¡todas las cuales tuvieron que ser burladas! Todavía no lo tengo todo resuelto, pero dividí la clase en tres clases, cada una de las cuales trataba con uno de los sustantivos por separado que tenía, y obtuve la lista de dependencia a 12 para el peor y alrededor de 8 para el el mejor.
La capacidad de prueba lo obligará a escribir un mejor código. Debería escribir pruebas unitarias porque descubrirá que le hace pensar en su código de manera diferente y escribirá un código mejor desde el principio, independientemente de los pocos errores que haya tenido antes de escribir las pruebas unitarias.
fuente
No estás literalmente violando la Ley de Deméter, pero tu problema es similar a eso de alguna manera. Dado que el objetivo de su pregunta es encontrar recursos, le sugiero que lea sobre la Ley de Demeter y vea cuánto de ese consejo se aplica a su situación.
fuente
Hay casos en los que lo mejor (en términos de eficiencia, facilidad de mantenimiento y facilidad de implementación) para tener ciertas variables como globales en lugar de la sobrecarga de siempre pasar todo (digamos que tiene aproximadamente 15 variables que deben persistir). Por lo tanto, tiene sentido encontrar un lenguaje de programación que admita mejor el alcance (como las variables estáticas privadas de C ++) para aliviar el desorden potencial (del espacio de nombres y la manipulación de las cosas). Por supuesto, esto es solo de conocimiento común.
Pero, el enfoque establecido por el OP es muy útil si uno está haciendo Programación Funcional.
fuente
No hay ningún antipatrón aquí en absoluto, porque la persona que llama no conoce todos estos niveles a continuación y no le importa.
Alguien está llamando al nivel más alto (params) y espera que el nivel más alto haga su trabajo. Lo que hace HighLevel con los parámetros no es asunto de quienes llaman. higherLevel maneja el problema de la mejor manera posible, en este caso pasando parámetros al nivel 1 (parámetros). Eso está absolutamente bien.
Ves una cadena de llamadas, pero no hay una cadena de llamadas. Hay una función en la parte superior que hace su trabajo de la mejor manera posible. Y hay otras funciones. Cada función puede ser reemplazada en cualquier momento.
fuente