Skip to Content
Technical Articles

ABAP Orientado a Objetos – Patrón Estrategia Ejemplo práctico

En esta entrada veremos como nos ayuda el patrón del estrategia en nuestros desarrollos.

Este patrón nos permite cambiar el algoritmo dependiendo del cliente.

Veamos primero el problema, después atacaremos la solución desde un punto de vista “tradicional” y después usaremos el patrón para dar la solución.

Enunciado del problema

En la contabilidad electrónica en México, se guarda la información del número único de la factura (UUID) en los textos de los documentos de finanzas. Como es un texto, esta información no se muestra en los reportes estándares de los documentos financieros, como son FBL1N ( facturas de proveedores) y FBL5N (Facturas de clientes).

Una opción para suplir esto, es usar la BAdI FI_ITEMS_CH_DATA, que implementa la interfaz IF_FI_ITEMS_CH_DATA y el método change_items.

Ésta BAdI, pasa por todos los procesos que llenan los reportes estándares de FI, en particular, en la ejecución de las transacciones FBLxN.

Necesitamos además, extender, la estructura que usa el método (pero será material para otra entrada), para nuestro caso, vamos a asumir, que ya tenemos la estructura extendida con el campo UUID (ZZUUID – char de longitud 36).

Dependiendo de si la factura es de proveedor (FBL1N) o si es de cliente (FBL5N) la forma de obtener el UUID es diferente:

  • En el caso de proveedores:
    • Leemos el texto de la factura ( id = YUUD ; object = BELEG ; name = bukrs&&belnr&&gjahr )
    • Si no encontramos, leer de RBKP con belnr = awkey(10) ghajr = awkey+10(4)
  • En el caso de clientes:
    • Leemos el texto de la factura de SD ( ( id = YUUD ; object = VBBK ; name = akkey(10) )
    • Si no encunetra datos, leemos el texto de la factura FI ( id = YUUD ; object = BELEG ; name = bukrs&&belnr&&gjahr )

Análisis del problema

Necesitamos un algoritmo que se comporte de forma diferente, dependiendo de la transacción que se esté ejecutando.

Una forma sería, de la forma tradicional, crear un método por cada caso, y usarlos en el método de la BAdI, haciendo el despliegue de las diferentes opciones, y llamar la método correcto en cada caso.

Otra forma, sería usar el patrón estrategia para solo tener que llamar a un método. Otra de las ventajas de usar el patrón, es que esto nos garantiza que estamos usando las mejores prácticas y cumpliendo con los diseños de orientado a objetos, como por ejemplo el de open/close, que dice, que los desarrollos deben ser abiertos a extensiones, pero cerrados a modificaciones.

En las soluciones, voy a obviar los parámetros de entrada de los método por razones de sencillez en la presentación del código, pero comentar que los métodos deben de pasar la llave del documento y al menos, el campo awkey de BKPF, para poder obtener los textos, según las condiciones del enunciado. Tampoco pongo el código de la implementación, ya que no afecta al diseño de la solución, que es lo que nos interesa ver.

 

Solución sin patrón estrategía.

Crearíamos los métodos en la misma clase de implementación (como privados) .

CLASS zim_if_items_ch_data DEFINITION.
* parte estándard de la clase, proveniente de la BAdI
PRIVATE SECTION.
  METHODS: obtener_uuid_prov RETURNING VALUE(r_text) TYPE zuuid,
           obtener_uuid_clie RETURNING VALUE(r_text) TYPE zuuid.
ENDCLASS.

Así definidos, en el método change_items nos quedaría así:

CASE sy-tcode.
  WHEN 'FBLN1'.
    LOOP AT ct_items ASSIGNING FIELD-SYMBOL(<l_items>).
      <l_items>-zzuuid = obtener_uuid_prov( ).
    ENDLOOP.
  WHEN 'FBL5N'.
    LOOP AT ct_items ASSIGNING <l_items>.
      <l_items>-zzuuid = obtener_uuid_cliente( ).
    ENDLOOP.
ENDCASE.

Pensando en el principio open/close nos queda que esta clase (la que implementa la BAdI) cada vez que hubiera un cambio en la forma de obtener el uuid, o si se necesita añadir el uuid para otra t-code, siempre tendríamos que hacer modificaciones a esta clase, violando este principio.

 

Solución  usando el patrón estrategía

En nuestro caso, el cliente, de la definición, sería la transacción y el algoritmo, la forma de obtener el UUID, así que nuestro problema es candidato para codificarlo con el patrón de estrategía.

Este es el diagrama de clases del patrón.

Traduciendo este diagrama a nuestro ejemplo:

Una interfaz, con el método (algoritmo) que se puede comportar de diferente forma, que sería:

INTERFACE lif_uuid.
  METHODS: obtener_uuid RETURNING VALUE(r_text) TYPE ZUUID.
ENDINTERFACE.

Y las clases que van a implementar el algoritmo de diferentes formas

CLASS lcl_fbl1n DEFINITION.
  INTERFACES: lif_uuid.
  ALIAS: obtener_uuid FOR lif_uuid~obtener_uuid.
ENDCLASS.

CLASS lcl_fbl5n DEFINITION.
  INTERFACES: lif_uuid.
  ALIAS: obtener_uuid FOR lif_uuid~obtener_uuid.
ENDCLASS.

El diagrama de clases, nos dice que el contexto “contiene” a la interface, nuestro contexto es la clase de implementación de la BAdI y el “contiene” se traduce en que en la clase se tiene que crear una instancia de la interfaz.

DATA: l_report TYPE REF TO lif_uuid.
l_report = NEW <clase_que_implementa_a_la_interfaz_uuid>( ).

Esta clase, la determinaremos en tiempo de ejecución dependiendo de la t-code.

Nota:

Podríamos haber creado un atributo de la clase, en vez de usar la variable local, pero creo que así se ve de forma más simple.

¿Cómo determinamos la clase que se va a instanciar?

Para ello vamos a hacer uso de otro patrón, el del método de fabricación.

Este patrón centraliza la creación de la clase y genera instancias dependiendo de los parámetros, en nuestro caso de la transacción que haya disparado.

Al método que “fabrica” las instancias, lo vamos a llamar set_report y le vamos a pasar la t-code y nos va a devolver una instancia de la interfaz lif_uuid.

CLASS lcl_fi_reports DEFINITION.
  CLASS_METHODS: set_report IMPORTING i_tcode TYPE sy-tcode
                            RETURNING VALUE(r_report) TYPE lif_uuid.
ENDCLASS.
CLASS lcl_fi_reports IMPLEMENTATION.
  METHOD set_report.
    r_report = SWITCH #( i_tcode
                         WHEN 'FBL1N' THEN NEW lcl_fbl1n( )
                         WHEN 'FBL5N' THEN NEW lcl_fbl5n( ) ).
  ENDMETHOD.
ENDCLASS.

Ahora ya tenemos todos los elementos y la solución final, se vería así:

DATA(l_report) = lcl_fi_reports=>set_report( sy-tcode ).
LOOP AT ct_items ASSIGNING FIELD-SYMBOL(<l_items>).
  <l_items>-zzuuid = l_report->obtener_uuid( ).
ENDLOOP.

 

Antes de acabar… ¿Qué pasa con nuestro código si se ejecuta otra transacción que no sea FBL1N o FBL5N?
En el caso del CASE, no hace nada… pero en la última solución, no se instanciaría ninguna clase, por lo que al llamar al método obtener_uuid lo estaría haciendo a una clase que no se ha instanciado, lo que nos dará un error en tiempo de ejecución (un DUMP).

Para evitarlo, necesitamos hacer uso de otro patrón, el del objeto nulo, que basicamente es crear una clase en la que la implementación del método, sea “no hacer nada”.

Para ello, creamos una nueva clase y la añadimos al método set_report.

CLASS lcl_blank DEFINITION.
  INTERFACES: lif_uuid.
   ALIAS: obtener_uuid FOR lif_uuid~obtener_uuid.
ENDCLASS.
CLASS lcl_blank IMPLEMENTATION.
  METHOD obtener_uuid.
    " lo dejamos en blanco
  ENDMETHOD.
ENDCLASS.

******en la clase lcl_fi_report******
  METHOD set_report.
    r_report = SWITCH #( i_tcode
                         WHEN 'FBL1N' THEN NEW lcl_fbl1n( )
                         WHEN 'FBL5N' THEN NEW lcl_fbl5n( )
                         ELSE NEW lcl_blank( ) ).
  ENDMETHOD.

Así, si la t-code NO es ni FBL1N, ni FBL5N, se instanciará una clase en la que al llamar al método obtener_uuid no va a hacer nada, y así no tenemos que modificar para nada el código del método de la clase de implementación change_items

Revisando este diseño, desde el punto de vista del principio open/close . Si tenemos que hacer un cambio en la forma de obtener el uuid, solo tendríamos que ir a la clase de la correspondiente transacción y si se añade alguna transacción más, crearíamos una nueva clase (extensíón) y añadiríamos la nueva instancia al método estático de la clase lcl_fi_report, set_report con lo que se cumple el principio, ya que la clase de implementación no la tocamos y siempre funcionará igual.

 

Conclusión

En esta entrada hemos visto, que la aplicación de patrones de diseño, ya conocidos, nos ayuda a tener un mejor diseño de nuestro código, cumpliendo con las mejores prácticas de codificación, ahorrando tiempo en el mantenimiento de los objetos de desarrollo.

Hagamos un repaso, de los patrones que hemos usado.

  • Patrón de estategia

Este patrón nos proporciona un diseño para el caso en que tengamos una acción, que se debe implementar de formas diferentes, por objetos diferentes. No es factible usar herencia, porque los objetos no tienen por qué tener relación entre ellos.

  • Patrón de Fabricación

Este patrón nos ayuda a crear instancias de objetos de la misma familia (o de un mismo comportamiento).

  • Patrón de Objeto nulo

Este patrón nos ayuda a no tener que estar preguntando si se ha instanciado un objeto, sino que creamos un objeto que no hace nada.

 

Espero que esta entrada les sea de utilidad.

saludos,

Adolfo

/
4 Comments
You must be Logged on to comment or reply to a post.