martes, 27 de abril de 2010

Código repetido: cálculo del valor en varias monedas

Siguiendo con la inteción de minimizar el código repetido en la base de conocimiento, me encuentro ante un problema que no tengo del todo claro como resolver. Capaz que a alguien se le ocurre alguna solución que no estoy viendo :)

En varios lugares de la KB, se manejan valores de productos en tres monedas, llamémosles &Moneda1, &Moneda2 y &Moneda3. Las monedas 1 y 2 son siempre distintas entre sí. La moneda 3 puede ser igual a la 1, igual a la 2, o no coincidir con ninguna.

Entonces, el código que se repite es el siguiente:
&Valor1 = udp(PAlgunProc, &Moneda1, [otros parámetros])
&Valor2 = udp(PAlgunProc, &Moneda2, [otros parámetros])
if &Moneda3 = &Moneda1
    &Valor3 = &Valor1
else
    if &Moneda3 = &Moneda2
        &Valor3 = &Valor2
    else
        &Valor3 = udp(PAlgunProc, &Moneda3, [otros parámetros])
    endif
endif
El procedimiento que se llama depende del contexto en que se esté. No es siempre el mismo. La propiedad que tiene es que es siempre más costoso que un if, y por lo tanto no quiero llamarlo para la &Moneda3 si coincide con alguna de las otras.

Lo ideal sería poder extraer este código,  y tener un programa que reciba las tres monedas, y un procedimiento que reciba un parámetro de tipo Moneda. El problema es que esto no se puede hacer, porque los programas tienen todos parámetros distintos...

¿A alguien se le ocurre una alternativa a repetir el código de arriba cada vez que se precisa?

Capaz que sería bueno tener una forma mejor para hacer invocaciones dinámicas de objetos...

Se me ocurre poder tener una variable de algún tipo especial, que pudiera asignar con una llamada a un procedimiento. Se podría sustituir la línea
&Valor1 = udp(PAlgunProc, &Moneda1, [otros parámetros])
por
&miProc = createProc(PAlgunProc, @Moneda, [otros parámetros], @Valor)
&Valor1 = udp(&miProc, &Moneda1)
¿Para que serviría esto? Así no parece tener demasiada ventaja, estoy escribiendo lo que podría hacer en una línea en dos...

Pero si vemos como quedaría en el ejemplo anterior, podría pasar la invocación con algunos parámetros instanciados y otros no a una función genérica:
&miProc = createProc(PAlgunProc, @Moneda, [otros parámetros], @Valor)
call(PCalcula3Monedas, &Moneda1, &Moneda2, &Moneda3, &miProc, &Valor1, &Valor2, &Valor3)
La  implementación del procedimiento Cacula3Monedas sería entonces
&Valor1 = udp(&miProc, &Moneda1)
&Valor2 = udp(&
miProc, &Moneda2)
if &Moneda3 = &Moneda1
    &Valor3 = &Valor1
else
    if &Moneda3 = &Moneda2
        &Valor3 = &Valor2
    else
        &Valor3 =
udp(&miProc, &Moneda3)
    endif
endif
Algunas consideraciones:
  1. El createProc sería una forma nueva de "invocar" un objeto, que construiría otro "objeto", con menos parámetros, que podría ser invocado en otro contexto. Además debería aparecer dentro de los objetos referenciados.
  2. Los parámetros que no le voy a pasar en el momento, es decir, lo que voy a diferir para cuando haga la invocación real, debería marcarlos de alguna forma. En el ejemplo, con una @.

11 comentarios:

  1. Hola,

    Para empezar usaría un CASE en vez de If Else If. Generalmente ayuda a alcarar que se está distinguiendo en 3 casos de una misma variable (&Moneda3).

    Si es el mismo código en todo el sistema, ¿Resulta muy incómodo pasar todos los posibles parámetros a PCalcula3Monedas? Y hacer el CASE ahí dentro.

    ResponderEliminar
  2. Me cayó la ficha de que la solución del comentario anterior no es tan sencilla.

    Veo el beneficio de la solución que planteás, estaría interesante, aunque generalmente Genexus no se caracterísa por tener esa clase de dinamismos :)

    La solución que usaría con Genexus tal cual está es la que te plantee, escribiendo algo así:

    prc:EligeProc:
    parm(&tipo, &Moneda [todos los parámetros], out:&Valor);

    Do Case
    Case &tipo = '1'
    &Valor = udp(proc1, &Moneda, [algunos aprametros])
    Case &tipo = '2'
    &Valor = udp(proc2, &Moneda, [algunos aprametros])
    ...
    EndCase

    prc:Calcula3Monedas:
    parm(&tipo, &Moneda, [todos los parametros], &Valor);

    &Valor1 = udp(EligeProc, &tipo, &Moneda1, [todos los parametros])
    &Valor2 = udp(EligeProc, &tipo, &Moneda2, [todos los parametros])
    Do Case
    Case &Moneda3 = &Moneda1
    &Valor3 = &Valor1
    Case &Moneda3 = &Moneda2
    &Valor3 = &Valor2
    Otherwise
    &Valor3 = udp(EligeProc, &tipo, &Moneda3, [todos los parametros])
    EndCase

    Y dependiendo del contexto tenés que elegir que parámetros mandás y que &tipo de cálculo hacés.
    No es elegante, pero te permite eliminar el código repetido. Lo malo es que si cambia algún parámetro de alguno de los procs, tenés que recorrer y modificar todos los que llaman a Calcula3Monedas.

    ResponderEliminar
  3. En una de esas no entendí el problema.

    yo lo que haría es que todos los procesos tengan una misma definición de parámetros.

    Parm(in:Moneda, in:Parameters(),out:Value);

    Luego la designación de cual programa usar lo hago por call dinámico dependiendo del contexto.
    Podría definir un programa con un Case que define cual programa llamar y hacer el llamado directamente.
    y si es que los parámetros se definen en el momento y son diferentes dependiendo del programa, los cargo en ese momento.

    Do Case
    Case &FltMoneda = &Pesos
    &Pgm = "CotPesos"
    &Parameter(1) = "Primero"
    &Parameter(2) = "Segundo"
    Do Case
    Case &FltMoneda = &Dolares
    &Pgm = "CotDolares"
    &Parameter(1) = "Primero Dolar"
    &Parameter(2) = "Segundo Solar"
    &Parameter(3) = "Tercero Dolar"
    OtherWise
    &Pgm = "CotOtras"
    &Parameter(1) = "Primero Otro"
    EndCase
    &Valor = UDP(&Pgm,&Parameter)

    Ojo que parameters puede ser una collection, un SDT o lo que mejor quieran usar.
    Luego cada pgm sabrá como leer ese vector/coleección/sdt para obtener los parámetros que necesita.

    En el fondo lo que entendí es que dependiendo la moneda es el programa utilizado, con una lista variable de parámetros dependiendo la moneda.

    ResponderEliminar
  4. Pablo:

    Lo del case es razonable, tiene la ventaja que quedan todas las asignaciones a &Moneda3 alineadas.

    El tema de pasar todos los parámetros a un proc genérico, es que pueden ser muchos.

    Existen varias entidades que puedo querer ver el valor en tres monedas: movimietos de stock, documentos, pedidos, órdenes de compra, etc.

    Además hay otras variantes, como por ejemplo quiero ver el valor de un producto, o de un producto en un determinado depósito, etc.

    Se podría hacer, pero no me convence demasiado. Tiene el problema que decías que si cambiás uno tenér que cambiar un montón de objetos que en principio no tenían nada que ver.

    ResponderEliminar
  5. David:

    Decías que "En el fondo lo que entendí es que dependiendo la moneda es el programa utilizado, con una lista variable de parámetros dependiendo la moneda."

    En realidad es al revés: dado el contexto donde se va a ejecutar, el procedimiento que se llama es el mismo para las tres monedas.

    El procedimiento depende del contexto (si estoy viendo el valor para un documento o para un pedido por ejemplo), y ese procedimiento se invoca dos o tres veces con todos los mismos parámetros, salvo la moneda que cambia.

    Lo de pasar todos los parámetros a una sola función, tiene el problema que le decía a Pablo.

    Y por último, los call dinámicos... Es todo un tema, la verdad que a mi no me gustan, básicamente porque perdés la posibilidad de ver las referencias entre objetos. Yo prefiero no usar call dinámicos.

    Por eso me parece una ventaja tener alguna función como decía en la nota (createProc en el ejemplo), que lo que le diga a GeneXus es "mirá que voy a llamar a este objeto, pero lo voy a llamar dinámicamente; anotate que acá tengo una referencia".

    ResponderEliminar
  6. Je je, me parece que podés esperar tranquilo a que a Artech se le ocurra implementar algo así.
    Sobre todo por el tema de tener algunos parámetros fijos y otros que quedan libres para usar cuando es necesario.

    Lo que proponés sería muy parecido a la famosa función lambda, en python sería así:
    funcion = lambda moneda: return PAlgunProc([otros parametros])

    ResponderEliminar
  7. Pablo: Por ahora soñar es grátis :) Quien te dice, en una de esas lo implementan...

    Claramente no es el estilo de GeneXus, y no se si entra en la filosofía de hacer las cosas más declarativas (se ve que yo sigo pensando más procedural), pero son de esas cosas que claramente aumentarían la productividad.

    ResponderEliminar
  8. Hola... si el tercer procedimiento que calcular el valor de Moneda3 es diferente para cada caso la única solución que veo dentro de Genexus hoy es dividir el problema en 2.

    1) Ejecutar el procedimiento costoso solo en caso que haga falta.
    2) Hacer un procedimiento para eliminar lo repetido en todos (Este esta caso la asignación de &Valor3)

    &Valor1 = udp(PAlgunProc, &Moneda1, [otros parámetros])
    &Valor2 = udp(PAlgunProc, &Moneda2, [otros parámetros])

    if &Moneda3 <> &Moneda1 AND &Moneda3 <> &Moneda2 // Solo ejecutar si es necesario
    &Valor3 = udp(PAlgunProc, &Moneda3, [otros parámetros])
    end
    PNormalizar(&Valor3, &Moneda3, &Moneda1, &Valor1, &Moneda2, &Valor2) // Elimina el codigo repetido

    //La funcion "Normalizar" seria...
    //parms(inout: &Valor, &M, &M1, &V1, &M2, &V2)
    do case
    case &M = &M1
    &Valor = &V1
    case &M = &M2
    &Valor = &V2
    endcase

    ResponderEliminar
  9. Alejandro: Me gustó la idea de "normalizar". No elimina todo el código repetido, pero igual mejora con respecto a lo que hay.

    ResponderEliminar
  10. Que te parece esta logica:

    &Valor1 = udp(PAlgunProc, &Moneda1, [otros parámetros])
    &Valor2 = udp(PAlgunProc, &Moneda2, [otros parámetros])
    &Moneda3 = iif(&Moneda3 = &Moneda1, &Valor1,iif(&Moneda3 = &Moneda2,&Valor2,udp(PAlgunProc, &Moneda3, [otros parámetros]) )

    Soy un viejo programador, pero novato en Genexus, jolm01@gmail.com

    ResponderEliminar
  11. jolm:

    Interesante... No se me había ocurrido poner un upd adentro de un iif... ¿Eso funciona?

    Igual el problema sigue estando, porque esas tres líneas de código se repiten, y el iif lo que hace es reducir la cantidad de líneas pero no la complejidad del código.

    ResponderEliminar