{0,0}

Algunas ideas sobre "The Law of Leaky Abstractions"

Algunas precauciones para no caer en la trampa de las abstracciones con fugas y por qué toda abstracción eventualmente te obliga a mirar debajo del capó. Una reflexión sobre el artículo de Joel Spolsky.

Al construir software, a menudo vamos a encontrar diferentes tipos de teorías, patrones y principios que nos ayudan a resolver problemas comunes. Algunos de ellos son más generales y otros son más específicos para un contexto o problema particular.

Cuando comenzamos a adentrarnos en el diseño de software aparecen conceptos como layered architecture, Domain Driven Design (DDD), Clean Architecture, entre otros. Así encontramos enunciados como:

“Aislar la capa de negocio de la persistencia garantiza que la lógica central permanezca independiente de la tecnología de la base de datos, lo que mejora la mantenibilidad, la capacidad de prueba y la portabilidad. Esto se logra mediante el uso de repositorios para encapsular el acceso a los datos, lo que permite que el modelo de dominio sea independiente de cómo se almacena o recupera.” - Repository Pattern

“Defina las entidades y los agregados en la capa de dominio, evitando que contengan anotaciones específicas de la base de datos.” - Domain-Driven Design (DDD)

“La capa de dominio define las interfaces del repositorio de datos, y la capa de persistencia las implementa, invirtiendo el flujo de dependencia para que apunte hacia la lógica de negocio.” - Dependency Inversion

Es así que siguiendo estas propuestas generamos código en cualquiera de sus formas (funciones, clases, componentes, adaptadores) que tienen como objetivo ocultar las particularidades y en parte la complejidad de la infraestructura subyacente.

Por lo tanto, a medida que más componentes de infraestructura y tecnologías se añaden a un proyecto, más abstracciones se generan y menos control tenemos sobre lo que se encuentra debajo del capó.

Pensemos en un proyecto donde tenemos tecnologías como un bus de eventos y/o un message broker, una base de datos, un sistema de almacenamiento de archivos, un sistema de caché, etc. Cada uno de estos componentes tiene una complejidad tecnológica según la tecnología utilizada y cada abstracción que generamos con la idea de hacer la lógica de negocio lo más agnostica e independiente de la implementación son una fuente potencial de problemas futuros.

De esto nos habla Joel Spolsky en su artículo “The Law of Leaky Abstractions” que fue publicado en 2002 pero sigue igual de vigente en el presente.

Pensemos en un ejemplo simple, conocido para la mayoría, un ORM.

Un ORM es una abstracción que desde la perspectiva del desarrollador tiene una finalidad clara: interactuar con distintas bases de datos utilizando el mismo modelo mental, las mismas entidades y operaciones similares. Guardar, actualizar o consultar datos debería ser conceptualmente equivalente sin importar el motor subyacente.

Bajo esta abstracción, trabajar con datos se vuelve similar a manipular objetos en memoria, persistirlos es tan simple como invocar un método y consultarlos se asemeja a acceder a propiedades de un objeto ya cargado.

Sin embargo, esta simplificación comienza a desdibujarse cuando aparecen los detalles propios de cada tecnología.

Por ejemplo, los distintos motores de base de datos tienen comportamientos diferentes en cuanto a:

  • Manejo de transacciones y niveles de aislamiento
  • Consistencia
  • Soporte para joins y relaciones complejas
  • Estrategias de indexación
  • Performance ante ciertos patrones de consulta

Lo que en un ORM parece una operación homogénea, en realidad puede traducirse en estrategias completamente distintas según el motor. Una consulta eficiente en un sistema relacional puede ser extremadamente costosa o incluso imposible en uno no relacional.

Aquí es donde la abstracción comienza a “filtrar”.

La idea de que estamos trabajando con un modelo uniforme se rompe cuando:

  • El rendimiento no es el esperado
  • Aparecen problemas de concurrencia
  • Las consultas del ORM generan más operaciones de las previstas
  • Determinadas operaciones no pueden expresarse de forma eficiente sin “escapar” del ORM

En estos casos, como desarrolladores nos vemos obligados a entender detalles que, en teoría, estaban abstraídos:

  • Cómo el motor ejecuta una consulta
  • Qué índices se están utilizando
  • Qué tipo de locks se están aplicando
  • Cómo se están materializando las relaciones

Es decir, la abstracción deja de ser suficiente y la realidad subyacente vuelve a aparecer.

El ORM intenta ocultar la complejidad de múltiples sistemas de almacenamiento bajo una única interfaz, pero no puede hacerlo de manera perfecta porque las diferencias entre esos sistemas son fundamentales, no accidentales.

Y este fenómeno no es exclusivo de los ORMs. Aparece también cuando tratamos de:

  • Modelar operaciones distribuidas como si fueran locales
  • Encapsular la red como si fuera una llamada a función
  • Representar sistemas concurrentes como si fueran secuenciales

En todos estos casos, la abstracción funciona bien hasta que deja de hacerlo.

Por eso, uno de los principales riesgos al aplicar patrones como Repository, DDD o Clean Architecture no está en los patrones en sí, sino en asumir que la abstracción que proponen es suficiente para todos los escenarios.

Cuando eso sucede, dejamos de diseñar teniendo en cuenta la realidad del sistema y comenzamos a diseñar en función de una representación idealizada del mismo.Y es en ese punto donde empiezan los problemas.

A raíz de esto es que quiero compartir algunas ideas sobre como podemos prevenir y/o mitigar estos problemas.

  1. Comprender la complejidad subyacente:

    • Antes de usar una abstracción, analiza cuidadosamente sus limitaciones y posibles consecuencias. Dedica el tiempo necesario a entender ventajas, desventajas y sus consecuencias, no solo de la abstracción que estás creando o utilizando, sino también de la infraestructura subyacente y las tecnologías elegidas.
    • Entendé que se está ocultando (aunque no lo uses ahora).
  2. Nunca confíes ciegamente en una abstracción:

    • Toda abstracción va a fallar en algún momento. Diseñá pensando en ese momento.
    • La red puede fallar, una solicitud puede llegar a un servidor ejecutarse sin recibir una respuesta adecuada ¿Cómo vas a resolver la idempotencia? ¿Cómo vas a resolver timeouts y retries?
  3. Elegí bien dónde vas a abstraer:

    • Presta especial atención a operaciones que son críticas para la consistencia de tu negocio, sensibles a la performance y la escalabilidad.
    • Por momentos, es preferible que tu lógica de negocio conozca que su persistencia es una base de datos PostgreSQL y la forma en la que puede resolver la concurrencia en lugar de confiar ciegamente en una abstracción que lo oculta o no se adapta a tus necesidades.
    • Es correcto abstraer operaciones repetitivas, estables o sin impacto crítico, por ejemplo: el logging, parsing de datos y otras utilidades.
  4. Mantené vías de escape:

    • Toda buena abstracción debería poder romperse cuando lo necesitás. Si una abstracción no puede satisfacer una necesidad específica, ofrece una alternativa clara.
    • No siempre es posible abstraer todo, por lo tanto, asegurate de que tu abstracción sea lo suficientemente flexible como para poder romperla cuando lo necesitás. Es por esto que algunos ORM tienen utilidades como rawQuery o executeRaw para poder ejecutar consultas SQL directamente.
    • Aceptá que puedes, vas (y probablemente te veas obligado a) mirar debajo del capó y eventualmente romper abstracciones.

Y recordá, no hay peor abstracción que la que te hace ignorar la realidad. La complejidad no desaparece, espera el momento para volver y allí podés ignorarla pero no evitar sus consecuencias.

Muchas gracias por leer, espero vernos pronto en otro artículo. Ya tengo algunas ideas en mente para compartir sobre soluciones específicas a estos problemas de los que hablamos hoy. ¡Nos vemos en la próxima!