Ventajas e inconvenientes de las estructuras de datos sin esquema

En mi anterior post introducía las estructuras de datos sin esquema como una alternativa a la forma de representar datos en memoria (no estoy hablando de bases de datos NoSQL) más frecuente en lenguajes como C# o Java, y explicaba que dependiendo del lenguaje podían resultar más o menos «naturales».

Ha llegado el momento de ver qué nos puede ofrecer esta forma de trabajar y, también, que problemas tiene.

Ventajas de usar estructuras de datos sin esquema

Para los que estamos acostumbrados a la red de seguridad que ofrece el compilador en un lenguaje de tipado estático, usar estructuras de datos sin esquema puede parece arriesgado, difícil de seguir e incluso «sucio». Sin embargo, el uso de este estilo de programación presenta algunas ventajas que merece la pena considerar.

Por una parte, al no tener que definir a priori un tipo para almacenar la información y aprovechar los tipos básicos del lenguaje, se consigue un menor acoplamiento entre los componentes del sistema.

Usando tipos definidos por el usuario, si queremos usar una clase o función que usa esos tipos, estaremos asumiendo una dependencia no sólo sobre la clase o función utilizada, sino también sobre los tipos usados por en la definición de esta (como parámetros o como valor de retorno), que a su vez pueden depender de otros tipos, que a su vez…

Al final es fácil llegar a un punto en que todo depende de todo y es difícil particionarlo (el que haya analizado alguna vez una base de código grande de C# usando NDepend sabe a lo que me refiero).

De hecho este es uno de los motivos por el que, incluso en lenguajes estáticos, muchas veces en los límites del sistema se usan tipos básicos para permitir fácilmente la integración de distintos sistemas sin tener que asumir dependencias de código entre unos u otros. Una alternativa al uso de tipos básicos es la serialización de nuestros tipos a formatos más interoperables como JSON o XML.

Otra ventaja de utilizar estas estructuras básicas es que permiten un mayor grado de reutilización de las funciones y métodos definidos. Esto es algo que va ligado también al tema del Duck Typing, pero incluso si estamos trabajando con datos puros (sin métodos) es también útil.

Por ejemplo, supongamos una función/método que hace algo con la propiedad Name del parámetro que recibe. Si la función recibe un map (tabla hash) con los valores, le bastará con obtener el valor de Name y operar con él. Si tenemos un map que representa un cliente y otro que representa un producto, podremos aplicar la función a ambos siempre que ambos contengan una clave Name. Si hubiésemos diseñado la función usando tipos predefinidos, necesitaríamos asegurar de alguna forma que todos los tipos sobre los que queramos aplicar la función implementan un interface común, lo que no siempre es posible (pensad en tipos que no están bajo vuestro control).

Una última ventaja de usar estructuras básicas es que permite ahorrar código porque no es necesario definir previamente la forma de los datos antes de usarlos. Esto es especialmente práctico cuando estamos trabajando con datos «de usar y tirar» que sólo se necesitan en un punto concreto de la aplicación y para los cuales definir una clase tiene poco sentido.

Inconvenientes de usar datos sin esquema

Desde mi punto de vista, el mayor problema de usar datos sin esquema es que no hay una única fuente a la que remitirse para saber qué se supone que contienen ni cómo deben utilizarse.

Cuando se trata de datos «de usar y tirar» como los que decía en el punto anterior, esto no es muy grave porque su vida suele ser corta, el código que los manipula está agrupado y es fácil seguirlo; pero, si se trata de información que se genera en un punto de la aplicación y va pasando por multitud de sitios, puedes acabar sin saber qué se supone que hay dentro del map que estas recibiendo.

Si a esto le unimos que este tipo de estructuras de datos son dinámicas por naturaleza y no puedes contar con el compilador para detectar errores cuando renombras una clave en una tabla hash, la cosa se pone aún más complicada.

Para intentar minimizar este problema es interesante intentar crear los datos en el menor número posible de puntos y, a ser posible, hacer que el código de esos puntos esté lo más agrupado posible. Así al menos se puede recurrir a ese código como documentación y comprobar si en el map que representa una persona hay una clave Name o si al final decidimos llamarla FullName.

Otro problema es que la propia naturaleza de este tipo de desarrollo hace que se intenten desacoplar datos y operaciones para poder reutilizar las operaciones con independencia de la procedencia de los datos, y esto no favorecen la agrupación de las operaciones relacionadas con los datos. No digo que no se pueda hacer, de hecho existen técnicas como módulos o espacios de nombres para conseguirlo, pero es fácil no prestarle mucha atención a eso y acabar ensuciando el espacio de nombres global con funciones demasiado específicas que sólo trabajan sobre un tipo de datos concreto y para un caso de uso específico.

Conclusiones

El hecho de haber trabajado tanto con C# me ha llevado a tener una visión muy sesgada de la forma correcta de hacer las cosas. Experimentar con otro tipo de lenguajes me ha permitido ver otras formas de trabajar que son interesantes y, en determinados casos, pueden resultar mucho más efectivas.

Es importante no perder de vista que cada lenguaje tiene su forma de hacer las cosas y lo mejor es aprovechar los puntos fuertes de cada plataforma. No tiene sentido escribir una aplicación en C# a base de pasar de un lado a otro arrays de strings y diccionarios (sería un caso claro de primitive obssesion ), pero tampoco es productivo desarrollar una aplicación en Clojure definiendo 300 clases.

El caso de Javascript es curioso, porque pese a su sistema de herencia prototípica, hay frameworks que postulan un esquema de desarrollo muy orientado a clases y a definir previamente todo lo que se va a usar, y hay otros que tratan de sacar partido a las características más dinámicas del lenguaje.