Table of Contents
Antecedentes
En los más de 10 años que he pasado en el desarrollo de software, he formulado una ley de depuración: «La perplejidad de un fallo de software y la simplicidad de su causa probable están positivamente correlacionadas». En pocas palabras, cuanto más confuso e «imposible» parece un fallo, más probable es que la razón subyacente del fallo no sea un caso extremo de pesadilla del compilador o un problema de hardware, sino algo que en realidad es bastante sencillo. A continuación se presentan dos casos que demuestran la ley en acción.
Caso #1: Python
Mi ejemplo de referencia para esto solía ser un error en un proyecto de Python que tardó casi un día entero en resolverse. A continuación se muestra una representación (muy simplificada) del código en cuestión:
def print_hello():
line = "Hello"
print(line)
Teniendo en cuenta las tres líneas de código involucradas, puedes imaginar que el programa que se ejecutaba imprimiendo constantemente el mensaje de error NameError: el nombre ‘línea’ no está definido en la línea de código print(línea) me hizo empezar a cuestionar mi cordura. Después de todo, la variable línea estaba definida en la línea *justo encima* de dónde se estaba usando; ¿cómo podía el intérprete de Python decir que la variable no existía? Finalmente, encontré la razón del error de una de las maneras más casuales: Abrí el archivo en un editor de texto que mostraba los espacios en blanco por defecto. De repente, el problema se hizo tan claro como el día:
ef print_hello():
line = "Hello"
····print(line)
Por ejemplo, la línea 2 estaba sangrada con un carácter de tabulación, mientras que la línea 3 estaba sangrada con cuatro espacios. Ese sonido que oyes es el de todos los veteranos de Python encogiéndose de hombros.
Para los menos familiarizados con Python, una rápida explicación: Python es un lenguaje de scripting famoso por evitar las llaves, los paréntesis, las palabras clave, etc. en favor de los espacios para definir los bloques de código. Aunque esto significa que el código de Python puede parecer más «limpio» que su equivalente en lenguajes como Java, Ruby o C++, también significa que la indentación del código ya no es simplemente una sugerencia de estilo: ahora determina cómo se ejecuta el código. En este caso, el evaluador de Python determinó que la línea con sangría tabulada = «Hola» estaba en su propio ámbito en comparación con el print(line) con sangría espacial, por lo que creó la variable line en la línea 2 para el ámbito con sangría tabulada; salió de ese ámbito cuando la línea había terminado de ejecutarse; y procedió a lanzar el NameError en la línea 3 porque ahora no había ninguna variable llamada line disponible. Por muy agravante que fuera ver un error tan simple delante de mí, también significaba que el error no era una ruptura de Python y que, en cambio, tenía la sencilla solución de sustituir el carácter de tabulación por cuatro espacios. Afortunadamente, el mundo de Python ha mejorado mucho desde este incidente: los IDEs modernos identificarán la discrepancia de espaciado, y Python 3 lanzará un TabError o un IndentationError (dependiendo del orden de las líneas con tabulador y espacio en el código) en lugar de un error desconcertante como el anterior.
Caso #2: Lombok
(¡Gracias especialmente a David García Folch por ayudar a descubrir y analizar este asunto!)
Los que hayan escrito código Java probablemente habrán trabajado con Lombok en al menos un proyecto durante su carrera. Para el resto: Lombok es una librería para Java que contiene un conjunto de anotaciones que se utilizan para generar código Java de tipo boilerplate. Para dar un ejemplo, el siguiente código:
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class SomeClass {
private String foo;
public void doSomething() {
System.out.println("Doing something");
}
}
procede a generar el siguiente código (compilado):
public class SomeClass {
private String foo;
public void doSomething() {
System.out.println("Doing something");
}
public SomeClass(String foo) {
this.foo = foo;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof SomeClass)) {
return false;
} else {
SomeClass other = (SomeClass)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$foo = this.getFoo();
Object other$foo = other.getFoo();
if (this$foo == null) {
if (other$foo != null) {
return false;
}
} else if (!this$foo.equals(other$foo)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(Object other) {
return other instanceof SomeClass;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $foo = this.getFoo();
result = result * 59 + ($foo == null ? 43 : $foo.hashCode());
return result;
}
public String toString() {
return "SomeClass(foo=" + this.getFoo() + ")";
}
}
Todo un ahorro de líneas de código. Entonces, ¿cómo hace Lombok para pasar de un código anotado a un código compilado completamente generado? Hace poco lo descubrí por las malas.
Imaginemos que la clase anotada por Lombok de arriba se utiliza de la siguiente manera en cincuenta lugares diferentes de un proyecto:
public class AnExampleClass {
public void launchSomething(String msg) {
var someClassObj = new SomeClass(msg);
System.out.println("Hello, " + someClassObj.getFoo());
}
}
Llega una tarea para añadir la siguiente anotación al código al método de una clase específica:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SomeAnnotation {
String aValue();
}
La anotación se añade a la siguiente clase:
public class OtherClass {
@SomeAnnotation
public void doSomething() {
System.out.println("Doing other thing");
}
}
De repente, el compilador de Java informa de una gran cantidad de errores, cincuenta de los cuales contienen el siguiente mensaje:
java: constructor SomeClass in class SomeClass cannot be applied to given types;
required: no arguments
found: java.lang.String
reason: actual and formal argument lists differ in length
¿Qué? Este código compiló perfectamente bien antes – y tu IDE no muestra ningún error en el código relacionado con el constructor de SomeClass – entonces, ¿por qué está fallando en todas partes ahora? La razón se debe a la forma en que Lombok se integra en el proceso de compilación de Java. Mientras que un desarrollador podría percibir la compilación de Java como una operación directa de traducción del código fuente de Java a código de bytes, lo que realmente ocurre es un proceso iterativo. En cada «ronda» de compilación, el compilador de Java analiza los errores de compilación que ha acumulado y determina si estos errores son recuperables. Si todos son recuperables, el compilador continúa. Así es como Lombok consigue que un código fuente que de otro modo sería «incorrecto» sobreviva a la primera pasada del compilador de Java: los errores relacionados con el código que Lombok aún tiene que generar se consideran recuperables, y Lombok genera entonces el código necesario para satisfacer al compilador en las rondas siguientes. ¿Y si el compilador encuentra un error irrecuperable? Se detiene el proceso y se imprimen *todos* los errores -tanto los que eran irrecuperables como los que podrían haber sido recuperables-, con lo que parece que Lombok se ha roto. Hay que reconocer que esto llevó mucho más tiempo de depuración que el problema de Python: fue necesario ejecutar el compilador de Java en modo de depuración; identificar un punto adecuado para insertar un punto de interrupción en el código del compilador, y recorrer el proceso de compilación hasta que se descubrió el eventual culpable en el código de OtherClass.
java: annotation @SomeAnnotation is missing a default value for the element 'aValue'
¡Oops! Al igual que antes, la solución era fácil – añadir un valor para aValue en la declaración de la anotación en OtherClass – después de lo cual todos los errores de compilación desaparecieron. Para estar seguros, este mensaje de error se imprimía desde el principio del problema, pero es fácil pasar por alto un mensaje de este tipo cuando está enterrado entre todos los mensajes de error relacionados con Lombok que se imprimen también en un proyecto que aprovecha mucho Lombok.
Conclusión
A primera vista, los dos casos descritos son bastante diferentes:
- Un proyecto fue escrito en Python, y el otro en Java.
- La causa de uno de los problemas se descubrió al activar la visualización de los caracteres de espacio en blanco, mientras que el otro requirió un paso más complicado del compilador de Java.
Sin embargo, lo que estos dos problemas tenían en común era que un error que en un principio parecía «imposible», al final se demostró que era un error bastante simple que requería una solución rápida. Es fácil empezar a imaginar que nos hemos encontrado con algún error de pesadilla que está siendo causado por un fallo en el lenguaje de programación, una memoria RAM que necesita ser reemplazada, nasal demons, etc., pero el estado de la industria del desarrollo de software es mucho más seguro de lo que nuestros temores podrían hacernos creer, especialmente para los lenguajes maduros como Java y Python que tienen décadas de historia a sus espaldas. Cuando nos encontramos con este tipo de problemas desconcertantes, puede merecer la pena relajarse, hacer una pausa para reflexionar -y tal vez ir a coger un pato de goma también- y tranquilizarse pensando que si el error está haciendo que te preguntes si 2+2=4, el problema real es probablemente algo mucho más simple y fácil de remediar al final.
Author
-
Software engineer with more than 10 years of experience working with different technologies such as Java, Docker, Kubernetes, React, CDK, Kotlin.... With high ability to deal with different environments and technologies.
Ver todas las entradas