¿Hay un nombre para el (anti-) patrón de pasar parámetros que solo se usará en varios niveles en la cadena de llamadas?

204

Estaba intentando encontrar alternativas al uso de la variable global en algún código heredado. Pero esta pregunta no es sobre 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 primero el parámetro.

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

donde newParam era anteriormente una variable global en mi ejemplo, pero podría haber sido un valor codificado previamente en su lugar. El punto es que ahora el valor de newParam se obtiene en higherlevel() y tiene que "viajar" hasta level3() .

Me preguntaba si había uno (s) nombre (s) para este tipo de situación / patrón en el que necesitas agregar un parámetro a muchas funciones que simplemente "pasan" el valor sin modificar.

Con suerte, el uso de la terminología adecuada me permitirá encontrar más recursos sobre soluciones para el rediseño y describir esta situación a los colegas.

    
pregunta ecerulm 31.10.2016 - 15:55

10 respuestas

198

Los datos en sí se llaman "datos de tramp" . Es un "olor a código", que indica que una pieza de código se está comunicando con otra pieza de código a distancia, a través de intermediarios.

  • Aumenta la rigidez del código, especialmente en la cadena de llamadas. Usted está mucho más restringido en la forma en que refactoriza cualquier método en la cadena de llamadas.
  • Distribuye el conocimiento sobre datos / métodos / arquitectura a lugares a los que no les importa lo más mínimo. Si necesita declarar los datos que está pasando, y la declaración requiere una nueva importación, ha contaminado el espacio de nombres.

Refactorizar para eliminar variables globales es difícil, y los datos de tramp es un método para hacerlo, y a menudo la forma más barata. Tiene sus costos.

    
respondido por el BobDalgleish 31.10.2016 - 17:26
98

No creo que esto, en sí mismo, sea un anti-patró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, digamos que necesito calcular el número de días entre dos fechas del calendario, así que creo una función:

int daysBetween(Day a, Day b)

Para hacer esto, creo una nueva función:

int daysSinceEpoch(Day day)

Entonces mi primera función se convierte simplemente:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

No hay nada anti-patrón sobre esto. Los parámetros del método daysBetween se transfieren a otro método y nunca se hace referencia a ellos en ningún otro modo, pero aún son necesarios para que ese método haga lo que tiene que hacer.

Lo que recomendaría es mirar cada función y comenzar con un par de preguntas:

  • ¿Esta función tiene una meta clara y enfocada o es un método de "hacer algunas cosas"? Por lo general, el nombre de la función ayuda aquí y si hay cosas en ella que no están descritas por el nombre, es una bandera roja.
  • ¿Hay demasiados parámetros? A veces, un método puede necesitar legítimamente muchos aportes, pero tener tantos parámetros hace que sea complicado de usar o entender.

Si está viendo una maraña de código sin un solo propósito agrupado en un método, debe comenzar por desentrañar eso. Esto puede ser tedioso. Comience con las cosas más fáciles de sacar y muévase a un método separado y repita hasta que tenga algo coherente.

Si tienes demasiados parámetros, considera Método para refactorizar objetos .

    
respondido por el JimmyJames 31.10.2016 - 16:21
59

BobDalgleish ya ha notado que este patrón (anti-) se llama "tramp data ".

En mi experiencia, la causa más común de un exceso de datos de tramp 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 correctamente los datos.

Para un ejemplo simple, considera un juego que tiene un personaje de jugador personalizable, con propiedades como playerName , playerEyeColor y así sucesivamente. 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 versió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 involucra al jugador. Por lo tanto, su estado global puede contener variables como:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

Pero en algún momento, es posible que necesites cambiar este diseño, quizás porque quieres 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 en particular en su juego podría involucrar una función de cadena de llamadas como, por ejemplo:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... y la función interactWithShopkeeper() hace que el comerciante se dirija al jugador por su nombre, por lo que ahora de repente necesitas pasar playerName como datos de trampa a través de todas esas funciones. Y, por supuesto, si el comerciante piensa que los jugadores de ojos azules son ingenuos, y cobrarán precios más altos por ellos, entonces deberá pasar playerEyeColor a través de toda la cadena de funciones, y así sucesivamente.

La solución adecuada , en este caso, es, por supuesto, definir un objeto de jugador que encapsule el nombre, el color de ojos, la posición, la salud y cualquier otra propiedad del personaje del jugador. De esa manera, solo necesitas 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 de jugador, lo que les daría acceso a las propiedades del jugador automáticamente. En cierto modo, esto es solo azúcar sintáctica, 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 un estado mucho más "global" que solo el jugador; por ejemplo, es casi seguro que tenga 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 quizás elementos que se encuentran en él, y así sucesivamente. También podrías pasar a todos aquellos como objetos vagabundos, pero eso volvería a saturar los argumentos de tu método.

En cambio, la solución es que los objetos almacenen referencias a cualquier otro objeto con el que tengan relaciones permanentes o temporales. Así, por ejemplo, el objeto jugador (y probablemente también cualquier objeto NPC) probablemente debería almacenar una referencia al objeto "mundo de juego", que tendría una referencia al nivel / mapa actual, de modo que un método como player.moveTo(x, y) sí tiene No es necesario que se le dé explícitamente el mapa como parámetro.

De manera similar, si nuestro personaje de jugador tuviera, digamos, 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 del jugador una referencia al perro (para que el jugador puede, digamos, llamar 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 objeten las dos subclases de un objeto "actor" más genérico, de modo que podamos reutilizar el mismo código para, digamos, mover ambos por el mapa.

Sal. Aunque he usado un juego como ejemplo, hay otros tipos de programas en los que también surgen problemas de este tipo. Sin embargo, en mi experiencia, el problema subyacente tiende a ser siempre el mismo: tienes 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 tramp" que se introducen en sus funciones consisten en configuraciones de opciones "globales" o consultas de base 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 conviértalo en un objeto (o lo que sea el equivalente más cercano en su idioma elegido).

    
respondido por el Ilmari Karonen 31.10.2016 - 23:39
33

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 un cierto tamaño

  • como parámetro puramente local, el alcance puede ser demasiado pequeño, cuando conduce a muchas listas de parámetros repetitivas en las cadenas de llamadas

  • por lo tanto, como compensación, a menudo se puede convertir un parámetro como una variable miembro en una o más clases, y eso es lo que yo llamaría diseño de clase adecuado .

respondido por el Doc Brown 31.10.2016 - 16:38
21

Creo que el patrón que está describiendo es exactamente inyección de dependencia . Varios comentaristas han argumentado que este es un patrón , no un anti-patrón , y tiendo 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 una caja negra que toma a 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 jalea, podría escribirlo como

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

pero sería mejor práctica aplicar la inyección de dependencia y escribirla así:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

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 verdadero que para make_sandwich necesita acceso a un Refrigerator ; por lo tanto, la firma de la antigua función era básicamente falsa al no tomar el refrigerador como parte de sus entradas.

¡Como bonificación, si haces correctamente la jerarquía de tu clase, evitas los cortes, etc., puedes incluso realizar una prueba unitaria de la función make_sandwich pasando un MockRefrigerator ! (Es posible que necesite realizar una prueba unitaria de esta manera porque su entorno de prueba unitaria podría no tener acceso a ningún PhysicalRefrigerator s.)

Comprendo que no todos los usos de la inyección de dependencia requieren instalar un parámetro de nombre similar en muchos niveles en la pila de llamadas, por lo que no estoy respondiendo a exactamente la pregunta pregunte ... pero si está buscando más información sobre este tema, "inyección de dependencia" es definitivamente una palabra clave relevante para usted.

    
respondido por el Quuxplusone 31.10.2016 - 22:27
15

Esta es prácticamente 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 esto es una mejora sobre lo 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 ser arreglado. Debería poder refactorizar para quitar o reducir el acoplamiento, aunque si ha estado allí durante un tiempo puede ser doloroso.

    
respondido por el Karl Bielefeldt 31.10.2016 - 17:16
3

Si bien esta respuesta no directamente responde a su pregunta, creo que sería negligente dejarla pasar sin mencionar cómo mejorarla (ya que, como usted dice, puede ser un anti- modelo). Espero que usted y otros lectores puedan obtener valor de este comentario adicional sobre cómo evitar los "datos de trampeo" (como Bob Dalgleish lo llamó 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 este paso de argumentos profundamente sin solo saltar a " simplemente pasar una clase en la que solía pasar muchos argumentos! " es refactorizar para que se realicen algunos pasos de su proceso en el nivel más alto en lugar del más bajo. Por ejemplo, aquí hay un código antes :

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

Tenga en cuenta que esto se vuelve aún peor cuanto más cosas se deben hacer en ReportStuff . Es posible que tenga que pasar la instancia del Reporter que desea usar. Y todo tipo de dependencias que deben ser entregadas, funcionan a la función anidada.

Mi sugerencia es llevar todo eso a un nivel más alto, 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:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

Observe 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 un cierto "aplanamiento" para que el proceso se vea como una serie de pasos en ese nivel, habrá hecho una mejora.

Aunque 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 se puede lograr al convertir algo en una clase. Las llamadas al método encadenado en el escenario antes 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 cosas que no debería, o hacer un método que haga demasiadas cosas violando así el principio de responsabilidad única, en general he encontrado que aplanar las cosas es un poco útil. 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. Las llamadas al método encadenado en realidad dificultan las pruebas unitarias más difíciles porque no tiene un buen punto de entrada y salida en el ensamblaje para el sector que desea probar. Tenga en cuenta que con este aplanamiento, ya que sus métodos ya no toman tantas dependencias, son más fáciles de probar, ¡y no requieren tantas burlas!

Recientemente traté de agregar pruebas unitarias a una clase (que no escribí) que tomó algo como 17 dependencias, ¡todas las cuales tuvieron que ser burladas! Aún no lo he resuelto todo, pero dividí la clase en tres clases, cada una de las cuales trata con uno de los nombres por los que estaba preocupada, y obtuve la lista de dependencias hasta 12 para el peor y 8 para el peor el mejor.

La capacidad de prueba lo forzará a escribir mejor código. Debería estar escribiendo pruebas unitarias porque encontrará que lo hace pensar en su código de manera diferente y escribirá un mejor código desde el principio, independientemente de los pocos errores que haya tenido antes de escribir las pruebas unitarias.

    
respondido por el ErikE 02.11.2016 - 04:03
1

No estás violando literalmente la Ley de Deméter, pero tu problema es similar a ese de alguna manera. Dado que el objetivo de su pregunta es encontrar recursos, le sugiero que lea sobre la Ley de Deméter y vea cuánto de ese consejo se aplica a su situación.

    
respondido por el catfood 31.10.2016 - 20:59
1

Hay casos en los que lo mejor (en términos de eficiencia, facilidad de mantenimiento y facilidad de implementación) es tener ciertas variables como globales en lugar de la sobrecarga de pasar siempre todo alrededor (digamos que tiene 15 o más variables que deben persistir). Por lo tanto, tiene sentido encontrar un lenguaje de programación que admita un mejor alcance (como las variables estáticas privadas de C ++) para aliviar el posible desorden (del espacio de nombres y la manipulación de las cosas). Por supuesto, esto es solo de conocimiento común.

Pero, el enfoque declarado por el OP es muy útil si uno está haciendo Programación Funcional.

    
respondido por el kozner 01.11.2016 - 05:18
0

Aquí no hay ningún patrón anti, porque la persona que llama no conoce todos estos niveles a continuación y no le importa.

Alguien está llamando a higherLevel (params) y espera que higherLevel haga su trabajo. Lo que upperLevel hace con params no es asunto del que llama. higherLevel maneja el problema de la mejor manera posible, en este caso pasando params a level1 (params). Eso es absolutamente bien.

Usted ve 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 podría ser reemplazada en cualquier momento.

    
respondido por el gnasher729 12.07.2017 - 10:28

Lea otras preguntas en las etiquetas