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é.
É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.
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í:
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.
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:
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:
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:
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
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 ).
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.
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:
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.
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:
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
Calle Eloy Gonzalo, 27
Madrid, Madrid.
Código Postal 28010
Paseo de la Reforma 26
Colonia Juárez, Cuauhtémoc
Ciudad de México 06600
Real Cariari
Autopista General Cañas,
San José, SJ 40104
Av. Jorge Basadre 349
San Isidro
Lima, LIM 15073