• Inicio
  • Novedades
  • Academia SAP
  • FAQ
  • Blog
  • Contacto
S4PCADEMY_Logo
Twitter Linkedin Instagram
S4PCADEMY_Logo
Twitter Linkedin Instagram
FORMACIÓN SAP

Chatp GPT, ayúdame a refactorizar mi clase

By s4pcademy 


Durante un taller, pedí a los concursantes que refactorizaran las fuentes de la Precio del teleférico Kata por Dominik Panzer. Hicimos tres rondas con aproximadamente 45 minutos por ronda y 15 desarrolladores calificados y experimentados intentaron identificar qué hace el código, escribir pruebas unitarias sensibles y refactorizar el código para mejorar su lectura y mantenimiento.

Esta mañana charlé con ChatGPT a ver, como me puede ayudar a resolver esta tarea. Hice preguntas, di instrucciones e intenté usar la salida en Eclipse.

Este es mi viaje.

Nota: chateé con ChatGPT en alemán y traduje la conversación al inglés usando ProfundoL, porque me da pereza volver a hacer lo mismo en inglés. No hice todo en el orden en que lo describo, pero el código generado y las respuestas son un resultado real de ChatGPT. Además, no reinicié el proceso al agregar los resultados a Eclipse para corregir errores. Yo mismo los arreglé.

Precios de remontes Kata

Él Precios de remontes Kata originalmente es un kata refactorizado por johan martinson para diferentes lenguajes de programación. El código es horrible, con errores y no muy bueno para leer.

El código calcula el precio de un pase de remonte en función de algunos parámetros.

La tarea es refactorizar el código usando pruebas unitarias. Para resolver esta tarea tendrás que identificar qué hace el código en primer lugar. Después de eso, puede decidir qué pruebas unitarias son adecuadas para esta clase. Si ha escrito suficientes pruebas unitarias para asegurarse de cubrir todas las combinaciones, puede refactorizar el código para mejorar su lectura y mantenimiento.

Escribir pruebas unitarias

Primero le pedí a ChatGPT que mostrara una lista de combinaciones sensatas para probar. Esta es la respuesta:

  • Hora del día “Día”:

    • Edad < 6: Resultado esperado: 0
    • Edad < 15: Resultado esperado: Precio base con 30% de descuento
    • Edad > 64: Resultado esperado: Precio base con 25% de descuento
    • Sin edad especificada: Resultado esperado: Precio base
  • Hora del día “Noche”:

    • Edad >= 6: Resultado esperado: Precio base con 40% de descuento
    • Edad < 6: Resultado esperado: 0

Puede ser útil agregar grupos de edad adicionales (p. ej., 15-64) y diferentes precios base para diferentes tipos de ascensores para ampliar las pruebas y verificar la precisión de los resultados.

Esa respuesta ayuda mucho para saber qué pruebas unitarias debo definir. Pero como soy perezoso, le pedí a ChatGPT que codificara las pruebas unitarias.

En primer lugar, solo me mostró el código para la prueba unitaria sin ningún nombre de método o definición de clase como esta:

* Test case for normal lift pass for adult
  DATA(age) = 30.
  DATA(type) = 'normal'.
  DATA(expected_cost) = 100.
  cl_zprices_1=>calculate_lift_pass_price( EXPORTING age = age
                                           type = type
                                           IMPORTING cost = DATA(actual_cost) ).
  cl_abap_unit_assert=>assert_equals( expected_cost, actual_cost ).

Esto no está mal, ya que ChatGPT usa la clase y el método correctos CL_ABAP_UNIT_ASSERT=>ASSERT_EQUALS. También utiliza la declaración en línea para definir las variables. No es necesario definir explícitamente las variables, pero para mí la solución está bien. Corresponde al común. DADO – CUANDO -ENTONCES lógica de prueba unitaria:

DADO – los datos de entrada

CUANDO – calcular el precio

ENTONCES – Espero lo siguiente (ASSERT_EQUALS)

La sintaxis del método ASSERT_EQUALS no es correcta. En ABAP tenemos que nombrar los parámetros que queremos usar. Por lo tanto, le pedí a ChatGPT que lo corrigiera.

Luego lo corrigió pero aún no ABAP-correct:

ASSERT_EQUALS( expected = 0 actual = cost message="Test for age less than 6 failed" ).

ChatGPT olvidó poner el nombre de la clase en la llamada ASSERT_EQUALS y usó nombres de parámetros incorrectos. Así que nuevamente pedí corrección («Los nombres de los parámetros son incorrectos»). Y ahora sucedió algo extraño: ChatGPT de repente imprimió una clase de prueba completa con definición de clase de prueba, nombres de métodos e implementación de métodos. Todavía usa los parámetros incorrectos y olvidó la palabra clave NIVEL DE RIESGO y los nombres de las polillas son demasiado largos, pero bueno, realmente ayuda. También hay un artefacto de algo que pregunté antes: crear un objeto de acceso a datos para reemplazar el comando SELECCIONAR. (Pero luego más sobre eso). ChatGPT mezcló la clase DAO y la clase principal. Pero eso es fácil de corregir. Ah, y también faltaba la adición PARA PRUEBAS en los métodos.

ChatGPT no me mostró el código completo. Creo que es por la limitación del texto. Pero acabo de enterarme de que podría haber escrito CONTINUAR para completar la respuesta.

CLASS lcl_zprices_1 DEFINITION FOR TESTING DURATION SHORT.
  PRIVATE SECTION.
    DATA: lv_dao TYPE REF TO zprices_1_dao.

    METHODS setup.
    METHODS teardown.
    METHODS test_calculate_lift_pass_price_day_under_6.
    METHODS test_calculate_lift_pass_price_day_6_to_15.
    METHODS test_calculate_lift_pass_price_day_over_15.
    METHODS test_calculate_lift_pass_price_night_under_6.
    METHODS test_calculate_lift_pass_price_night_6_to_64.
    METHODS test_calculate_lift_pass_price_night_over_64.
ENDCLASS.

CLASS lcl_zprices_1 IMPLEMENTATION.

  METHOD setup.
    CREATE OBJECT lv_dao TYPE zprices_1_dao.
    lv_dao->set_base_price( 'day', 100 ).
    lv_dao->set_base_price( 'night', 80 ).
  ENDMETHOD.

  METHOD teardown.
  ENDMETHOD.

  METHOD test_calculate_lift_pass_price_day_under_6.
    DATA(lv_cost) = lv_dao->calculate_lift_pass_price( age = 5, type="day" ).
    cl_abap_unit_assert=>assert_equals( expected = 0, actual = lv_cost msg = 'Price for day ticket under 6 years should be 0' ).
  ENDMETHOD.

  METHOD test_calculate_lift_pass_price_day_6_to_15.
    DATA(lv_cost) = lv_dao->calculate_lift_pass_price( age = 10, type="day" ).
    cl_abap_unit_assert=>assert_equals( expected = 70, actual = lv_cost msg = 'Price for day ticket between 6 and 15 years should be 30% off' ).
  ENDMETHOD.

  METHOD test_calculate_lift_pass_price_day_over_15.
    DATA(lv_cost) = lv_dao->calculate_lift_pass_price( age = 20, type="day" ).
    cl_abap_unit_assert=>assert_equals( expected = 100, actual = lv_cost msg = 'Price for day ticket over 15 years should be full price' ).

    DATA(lv_cost) = lv_dao->calculate_lift_pass_price( age = 65, type="day" ).
    cl_abap_unit_assert=>assert_equals( expected = 75, actual = lv_cost msg = 'Price for day ticket over 64 years should be 25% off' ).
  ENDMETHOD.
...

Copié este código para Eclipse e hice algunas correcciones para que funcione.

Estas son las cosas que corregí:

  • Añadido NIVEL DE RIESGO INOFENSIVO a la definición de clase.
  • Cambió los nombres de los métodos.
    • de TEST_CALCULATE_LIFT_PASS_PRICE_*
    • a CALCULAR_PRICE_*
  • Añadido FOR TESTING a los métodos de prueba
  • Cambié las cosas de LV_DAO a CUT con el nombre de mi clase actual.
  • agregó el caso de prueba Calculate_Price_Day_Over_64 que se colocó en el método Calculate_Price_Day_over_15
  • se agregaron los casos de prueba que se definieron pero falta la implementación

¡¡¡Oh espera!!!

Me acabo de dar cuenta de que solicité algo de ChatGPT que no pudo entregar: el precio base del pase de elevación está oculto en la base de datos. Por lo tanto, tiene mucho sentido que ChatGPT use un atributo DAO para establecer los precios base supuestos para el día y la noche:

CREATE OBJECT lv_dao TYPE zprices_1_dao.
lv_dao->set_base_price( 'day', 100 ).
lv_dao->set_base_price( 'night', 80 ).

Sin embargo, el objeto de acceso a datos no se utilizó correctamente. Tampoco expliqué que comencé pidiéndole a ChatGPT que refactorizara el código y extrajera la selección de la base de datos en un objeto de acceso a datos. Así que solo agregaré lo que ChatGPT y yo hicimos allí antes.

Objeto de acceso a datos

Un DAO – Objeto de acceso a datos es una clase que solo se preocupa por proporcionar y guardar datos. Es una técnica común para que coincida con el principio de diseño. Separación de preocupaciones (SoC) y usar datos falsos para pruebas unitarias (Inyección de dependencia).

El proceso es:

  • definir una interfaz para el DAO que tiene métodos para establecer y obtener datos
  • cree una clase que implemente la interfaz DAO y use una selección para acceder a los datos deseados
  • Cree una clase (local) que implemente la interfaz DAO y use datos falsos para realizar pruebas
  • Usar la interfaz DAO como atributo de la clase principal
  • Agregue un CONSTRUCTOR que tenga la interfaz DAO como parámetro de importación
  • en el CONSTRUCTOR, el atributo DAO debe crearse con la clase DAO que hace el normal selección de datos
  • en las pruebas unitarias, se puede crear una instancia de la clase principal al proporcionar una clase DAO que usa datos falsos

Con esta técnica puedes deshacerte de la dependencia “selección de datos”.

Le pedí a ChatGPT que hiciera esto y lo hizo en tres intentos que no fueron consistentes. Pero al menos hizo lo siguiente:

  1. agregue un atributo de clase a la clase principal:
    DATOS dobj TIPO REF A si_precio_dao.
  2. entregó el código para el DAO:
INTERFACE if_price_dao.
  METHODS: get_base_price IMPORTING type TYPE char10
                              RETURNING value(base_price) TYPE int4.
ENDINTERFACE.

CLASS lcl_price_dao_impl DEFINITION IMPLEMENTING if_price_dao.
  METHOD get_base_price.
    SELECT SINGLE cost INTO base_price FROM zbase_prices WHERE type = type.
  ENDMETHOD.
ENDCLASS.

Entonces ChatGPT usó el concepto DAO en la clase de prueba generada. No lo usó correctamente, porque la referencia DAO creada debería pasarse al CONSTRUCTOR de la clase principal. ChatGPT utiliza el método (aún no generado) SET_BASE_PRICE para definir la suposición del cálculo.

No es consistente en absoluto, pero sé lo que quería lograr y ChatGPT hizo mucho para que pudiera usarlo.

A continuación, intentaré agregar la interfaz DAO en el Tipos locales relevantes para la clase sección de mi clase e implementar el código para seleccionar datos. Hubo algunas adaptaciones que tuve que hacer:

  • corregir la sintaxis
  • añadir método SET_BASE_PRICE
  • agregar definición de clase e implementación para prueba DAO
INTERFACE if_price_dao.
  METHODS get_base_price
    IMPORTING
      type              TYPE char10
    RETURNING
      VALUE(base_price) TYPE int4.
  METHODS set_base_price
    IMPORTING
      type       TYPE char10
      base_price TYPE int4.
ENDINTERFACE.

CLASS lcl_price_dao_impl DEFINITION.
  PUBLIC SECTION.
    INTERFACES zif_price_dao.
ENDCLASS.

CLASS lcl_price_dao_test DEFINITION.
  PUBLIC SECTION.
    INTERFACES zif_price_dao.
  PRIVATE SECTION.
    TYPES: BEGIN OF _rate,
             type  TYPE char10,
             price TYPE i,
           END OF _rate,
           _rates TYPE SORTED TABLE OF _rate WITH UNIQUE KEY type.
    DATA rates TYPE _rates.
ENDCLASS.

La implementación debe colocarse en el Tipos locales sección de la clase:

CLASS lcl_price_dao_impl IMPLEMENTATION.
  METHOD if_price_dao~get_base_price.
    SELECT SINGLE cost INTO base_price FROM zbase_prices WHERE type = type.
  ENDMETHOD.

  METHOD if_price_dao~set_base_price.
    "not needed here
  ENDMETHOD.
ENDCLASS.

CLASS lcl_price_dao_test IMPLEMENTATION.
  METHOD zif_price_dao~get_base_price.
    base_price = VALUE #( rates[ type = type ]-price DEFAULT 0 ).
  ENDMETHOD.

  METHOD zif_price_dao~set_base_price.
    ASSIGN rates[ type = type ]-price TO FIELD-SYMBOL(<price>).
    IF sy-subrc = 0.
      <price> = base_price.
    ELSE.
      INSERT VALUE #( type = type price = base_price ) INTO TABLE rates.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

Normalmente no usaría un método SET para definir los valores de prueba, pero me gusta la idea y lo probaré.

Para la prueba DAO, tuve que hacer una implementación más para mantener las tarifas para día y noche.

Al menos necesito la posibilidad de configurar el DAO para la clase principal. Por lo tanto, agrego el atributo DAO y creo un constructor:

  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        dao TYPE REF TO if_price_dao OPTIONAL.
  PRIVATE SECTION.
    DATA dao TYPE REF TO if_price_dao.
  METHOD constructor.
    IF dao IS SUPPLIED.
      me->dao = dao.
    ELSE.
      me->dao = NEW lcl_price_dao_impl( ).
    ENDIF.
  ENDMETHOD.

Desafortunadamente me sale el siguiente error:

El tipo «IF_PRICE_DAO» es desconocido

Recuerdo que hubo algún problema y que hubo solución, pero ya no recuerdo. Para superar este error, creé la interfaz DAO globalmente: ZIF_PRICE_DAO

Volver a las pruebas

Ahora que implementé todas las cosas de DAO, agregué la llamada al método SETUP de mi clase de prueba:

  METHOD setup.
    DATA(dao) = CAST zif_price_dao( NEW lcl_price_dao_test( ) ).
    dao->set_base_price( type="day" base_price = 100 ).
    dao->set_base_price( type="night" base_price = 80 ).
    cut = NEW #( dao ).
  ENDMETHOD.

También tuve que reemplazar la selección directa usando el objeto de acceso a datos:

   DATA(ld_baseprice) = dao->get_base_price( type ).

Arrancar los motores

Bueno. Es hora de ejecutar las pruebas unitarias (parcialmente) generadas (CTRL-SHIFT-F10).

Este es el resultado:

No intentaré averiguar por qué este caso de prueba es incorrecto. En cambio, asumo que ChatGPT no descubrió los valores correctos y simplemente adaptó el caso de prueba.

Ahora todavía faltan dos casos de prueba que completaré manualmente y usaré los valores que reciba:

  METHOD calculate_price_night_over_64.
    DATA(lv_cost) = cut->calculate_lift_pass_price( age = 65 type="night" ).
    cl_abap_unit_assert=>assert_equals( exp = 32 act = lv_cost msg = 'Price for night ticket should be reduced' ).
  ENDMETHOD.

  METHOD calculate_price_night_under_6.
    DATA(lv_cost) = cut->calculate_lift_pass_price( age = 4 type="night" ).
    cl_abap_unit_assert=>assert_equals( exp = 0 act = lv_cost msg = 'Price for child night ticket should be free' ).
  ENDMETHOD.

refactorización

Ahora llegamos a la tarea principal: refactorizar el código. Le pedí a ChatGPT que

ahora cambie el código para que la lógica sea más fácil de entender y leer

El primer intento tuvo el resultado de que solo se descartaron las declaraciones CONV inútiles. El segundo intento tampoco fue muy bueno, así que dije:

la lógica sigue siendo difícil de leer. intentar otra vez.

Y esta es la salida:

CLASS zprices_1 DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .
  PUBLIC SECTION.

    METHODS calculate_lift_pass_price IMPORTING
      i_type TYPE char10
      i_age  TYPE int4 OPTIONAL
    RETURNING VALUE(r_cost) TYPE int4.
  PRIVATE SECTION.
    CLASS-METHODS calculate_discount
      IMPORTING
        i_age  TYPE int4
      RETURNING VALUE(r_discount) TYPE f.
    CLASS-DATA: 
      c_discount_night TYPE f VALUE '0.4',
      c_discount_kids TYPE f VALUE '0.3',
      c_discount_senior TYPE f VALUE '0.25'.
    INTERFACES if_pricing_dao.
    DATA:
      m_dao TYPE REF TO if_pricing_dao.
ENDCLASS.

CLASS zprices_1 IMPLEMENTATION.

  METHOD calculate_lift_pass_price.
    DATA:
      l_base_price TYPE int4.
    m_dao->get_base_price( EXPORTING i_type = i_type IMPORTING e_base_price = l_base_price ).
    IF l_base_price IS INITIAL.
      r_cost = 0.
    ELSE.
      r_cost = CEIL( l_base_price * ( 1 - calculate_discount( i_age ) ) ).
    ENDIF.
  ENDMETHOD.

  CLASS-METHOD calculate_discount.
    IF i_age < 6.
      r_discount = 0.
    ELSEIF i_age < 15.
      r_discount = c_discount_kids.
    ELSEIF i_age > 64.
      r_discount = c_discount_senior.
    ELSEIF i_type="night".
      r_discount = c_discount_night.
    ELSE.
      r_discount = 0.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

INTERFACE if_pricing_dao.
  METHODS get_base_price
    IMPORTING
      i_type TYPE char10
    EXPORTING
      e_base_price TYPE int4.
ENDINTERFACE.

CLASS zbase_prices_dao DEFINITION IMPLEMENTING if_pricing_dao.
  METHOD if_pricing_dao~get_base_price.
    SELECT SINGLE cost INTO e_base_price
    FROM zbase_prices
    WHERE type = i_type.
  ENDMETHOD.
ENDCLASS.

Tuve que hacer algunas pequeñas correcciones:

  • los nombres de los parámetros comenzaron ahora con i_
  • el método DAO GET_BASE_PRICE tenía un parámetro de importación en lugar de un parámetro de retorno
  • el TIPO no estaba disponible en el método CALCULATE_DISCOUNT.
    • Tenía las siguientes dos opciones:
      • añadir TIPO como parámetro de entrada
      • establecer TIPO como atributo global
    • Por alguna razón, decidí usar un atributo global.

Ahora tenía el siguiente código refactorizado:

CLASS zprices_1_chatgpt DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    METHODS constructor
      IMPORTING
        dao TYPE REF TO zif_price_dao OPTIONAL.

    METHODS calculate_lift_pass_price IMPORTING
                                                type          TYPE char10
                                                age           TYPE int4 OPTIONAL
                                      RETURNING VALUE(r_cost) TYPE int4.

  PRIVATE SECTION.
    METHODS calculate_discount
      IMPORTING
                age               TYPE int4
      RETURNING VALUE(r_discount) TYPE f.
    DATA:
      c_discount_night  TYPE f VALUE '0.4',
      c_discount_kids   TYPE f VALUE '0.3',
      c_discount_senior TYPE f VALUE '0.25'.
    DATA:
      type  TYPE char10,
      m_dao TYPE REF TO zif_price_dao.

ENDCLASS.



CLASS zprices_1_chatgpt IMPLEMENTATION.

  METHOD constructor.
    IF dao IS SUPPLIED.
      m_dao = dao.
    ELSE.
      m_dao = NEW lcl_price_dao_impl( ).
    ENDIF.
  ENDMETHOD.

  METHOD calculate_lift_pass_price.
    DATA:
      l_base_price TYPE int4.
    l_base_price = m_dao->get_base_price( EXPORTING type = type ).

    IF l_base_price IS INITIAL.
      r_cost = 0.
    ELSE.
      r_cost = ceil( l_base_price * ( 1 - calculate_discount( age ) ) ).
    ENDIF.

    me->type = type.
  ENDMETHOD.

  METHOD calculate_discount.
    IF age < 6.
      r_discount = 0.
    ELSEIF age < 15.
      r_discount = c_discount_kids.
    ELSEIF age > 64.
      r_discount = c_discount_senior.
    ELSEIF type="night".
      r_discount = c_discount_night.
    ELSE.
      r_discount = 0.
    ENDIF.

  ENDMETHOD.
ENDCLASS.

También le pregunté a ChatGPT qué hacía. Respondió:

He intentado mejorar la lógica del código reestructurando las consultas if-then-else y usando variables con nombres significativos. Esto incluye la eliminación de ramas innecesarias y el uso de bucles para simplificar los cálculos repetidos. También he intentado eliminar conversiones de tipo de datos innecesarias y mejorar el uso de constantes en lugar de codificación rígida. Además, traté de modularizar la lógica creando una clase «DAO» (objeto de acceso a datos) y usando inyección de dependencia para mejorar la mantenibilidad del código. También traté de crear la interfaz para él y eliminar las variables no utilizadas.

Quieres saber el resultado?? 😉 Aquí está

Nosotros, ChatGPT y yo, tenemos dos errores. No dos malos para el comienzo, pero también margen de mejora.

Conclusión

No quiero profundizar más ahora, porque AI y yo produjimos suficiente código y texto por hoy. Cuando probé ChatGPT para la refactorización, me sorprendieron los resultados. Después de haberlos revisado sigo impresionado, pero también veo que hay mucho trabajo manual. Sin embargo, la IA puede ayudar a obtener ideas o un punto de partida incluso para tareas de programación.

Esto es lo que encontré fue extraño:

  • aunque ChatGPT reacciona a la entrada anterior y se refiere a esto, produce resultados inconsistentes, como que el parámetro cambia de TYPE a I_TYPE y AGE a I_AGE.
  • Parece saber qué es código y qué es texto, pero a veces…
  • Al final, le pedí a ChatGPT que comentara el código original y dijo que no puede recordar el código original, así que tuve que publicarlo nuevamente. 😅

En mi opinión, es un gran logro lo que la IA es capaz de hacer.

Tengo mucha curiosidad acerca de los próximos pasos de evolución de ChatGPT.

Hasta ahora podemos ver la IA como una herramienta y no (todavía) como una amenaza.

¡Qué tenga un buen fin de semana!

~Enno




Formas en que SAP MII e Industry 4.0 mejoran los procesos de fabricación
Previo
Impresión de etiquetas de contenedores de almacenamiento en SAP-EWM:
Siguiente

Madrid

Calle Eloy Gonzalo, 27
Madrid, Madrid.
Código Postal 28010

México

Paseo de la Reforma 26
Colonia Juárez,  Cuauhtémoc
Ciudad de México 06600

Costa Rica

Real Cariari
Autopista General Cañas, 
San José, SJ 40104

Perú

Av. Jorge Basadre 349
San Isidro
Lima, LIM 15073

Twitter Linkedin Instagram
Copyright 2022 | All Right Reserved.